//+------------------------------------------------------------------+
//|                                                LadderExecutor.mqh|
//|                                Copyright 2026, Tola Moses Hector |
//|                                     https://t.me/tolahector      |
//+------------------------------------------------------------------+
#ifndef LADDEREXECUTOR_MQH
#define LADDEREXECUTOR_MQH

#include <Trade\Trade.mqh>
#include "ExitLadderCalibrator.mqh"

//+------------------------------------------------------------------+
//| CLadderExecutor                                                  |
//| Applies a calibrated SLadderRung[] ladder to open positions,     |
//| firing each rung at most once per ticket and optionally moving   |
//| the stop to breakeven after the first partial close.             |
//+------------------------------------------------------------------+
class CLadderExecutor
  {
private:
   struct SManaged
     {
      ulong             ticket;
      double            entry_price;
      double            r_distance;
      double            original_volume;
      long              type;
      bool              rung_fired[8];   // supports up to 8 rungs
      bool              breakeven_done;
     };

   SManaged          m_managed[];
   int               m_count;
   SLadderRung       m_ladder[];
   int               m_rung_count;
   bool              m_move_to_breakeven_after_first;
   CTrade            m_trade;

   int               FindManaged(ulong ticket) const;

public:
                     CLadderExecutor(void);
                    ~CLadderExecutor(void);

   void              Configure(const bool move_to_breakeven_after_first, const ulong magic);
   void              SetLadder(const SLadderRung &ladder[], const int rung_count);

   bool              Register(ulong ticket, double entry_price, double stop_price,
                              double volume, long type);
   bool              Deregister(ulong ticket);
   bool              IsRegistered(ulong ticket) const;

   void              OnTick(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CLadderExecutor::CLadderExecutor(void) : m_count(0),
   m_rung_count(0),
   m_move_to_breakeven_after_first(true)
  {
   ArrayResize(m_managed, 16);
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CLadderExecutor::~CLadderExecutor(void)
  {
  }
//+------------------------------------------------------------------+
//| Configure                                                        |
//+------------------------------------------------------------------+
void CLadderExecutor::Configure(const bool move_to_breakeven_after_first, const ulong magic)
  {
   m_move_to_breakeven_after_first = move_to_breakeven_after_first;
   m_trade.SetExpertMagicNumber(magic);
  }
//+------------------------------------------------------------------+
//| SetLadder                                                        |
//+------------------------------------------------------------------+
void CLadderExecutor::SetLadder(const SLadderRung &ladder[], const int rung_count)
  {
   int n = (rung_count > 8 ? 8 : rung_count); // cap at SManaged.rung_fired capacity
   ArrayResize(m_ladder, n);
   for(int i = 0; i < n; i++)
      m_ladder[i] = ladder[i];
   m_rung_count = n;
  }
//+------------------------------------------------------------------+
//| FindManaged                                                      |
//+------------------------------------------------------------------+
int CLadderExecutor::FindManaged(ulong ticket) const
  {
   for(int i = 0; i < m_count; i++)
      if(m_managed[i].ticket == ticket)
         return(i);
   return(-1);
  }
//+------------------------------------------------------------------+
//| IsRegistered                                                     |
//+------------------------------------------------------------------+
bool CLadderExecutor::IsRegistered(ulong ticket) const
  {
   return(FindManaged(ticket) >= 0);
  }
//+------------------------------------------------------------------+
//| Register                                                         |
//+------------------------------------------------------------------+
bool CLadderExecutor::Register(ulong ticket, double entry_price, double stop_price,
                               double volume, long type)
  {
   double dist = (type == POSITION_TYPE_BUY) ? (entry_price - stop_price) : (stop_price - entry_price);
   if(dist <= 0.0 || volume <= 0.0)
      return(false);

   if(FindManaged(ticket) >= 0)
      return(true);

   if(m_count >= ArraySize(m_managed))
      ArrayResize(m_managed, ArraySize(m_managed) * 2);

   int idx = m_count;
   m_managed[idx].ticket          = ticket;
   m_managed[idx].entry_price     = entry_price;
   m_managed[idx].r_distance      = dist;
   m_managed[idx].original_volume = volume;
   m_managed[idx].type            = type;
   m_managed[idx].breakeven_done  = false;
   for(int r = 0; r < 8; r++)
      m_managed[idx].rung_fired[r] = false;

   m_count++;
   return(true);
  }
//+------------------------------------------------------------------+
//| Deregister                                                       |
//+------------------------------------------------------------------+
bool CLadderExecutor::Deregister(ulong ticket)
  {
   int idx = FindManaged(ticket);
   if(idx < 0)
      return(false);

   m_count--;
   if(idx < m_count)
      m_managed[idx] = m_managed[m_count];
   return(true);
  }
//+------------------------------------------------------------------+
//| OnTick                                                           |
//| For every managed ticket, computes current progress in R and     |
//| fires any rung whose threshold has been reached and has not      |
//| already fired for that ticket.                                   |
//+------------------------------------------------------------------+
void CLadderExecutor::OnTick(void)
  {
   for(int i = m_count - 1; i >= 0; i--)
     {
      ulong ticket = m_managed[i].ticket;

      if(!PositionSelectByTicket(ticket))
        {
         Deregister(ticket);
         continue;
        }

      string symbol         = PositionGetString(POSITION_SYMBOL);
      double current_volume = PositionGetDouble(POSITION_VOLUME);
      double bid             = SymbolInfoDouble(symbol, SYMBOL_BID);
      double ask             = SymbolInfoDouble(symbol, SYMBOL_ASK);

      double favorable_price = (m_managed[i].type == POSITION_TYPE_BUY) ? bid : ask;
      double excursion = (m_managed[i].type == POSITION_TYPE_BUY)
                         ? (favorable_price - m_managed[i].entry_price)
                         : (m_managed[i].entry_price - favorable_price);
      double progress_r = excursion / m_managed[i].r_distance;

      for(int r = 0; r < m_rung_count; r++)
        {
         if(m_managed[i].rung_fired[r])
            continue;
         if(progress_r < m_ladder[r].r_level)
            continue;

         double close_volume = m_managed[i].original_volume * m_ladder[r].close_fraction;
         double vol_step     = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
         double vol_min      = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
         close_volume = MathFloor(close_volume / vol_step) * vol_step;

         //--- never close more than what remains open, and skip a rung
         //--- outright if the rounded volume falls below the broker
         //--- minimum rather than risk closing the entire remainder early
         if(close_volume > current_volume)
            close_volume = current_volume;
         if(close_volume < vol_min)
           {
            m_managed[i].rung_fired[r] = true; // mark handled, nothing to close
            continue;
           }

         if(m_trade.PositionClosePartial(ticket, close_volume))
           {
            m_managed[i].rung_fired[r] = true;
            PrintFormat("LadderExecutor: ticket=%I64u rung=%d R=%.2f closed=%.2f lots",
                        ticket, r, m_ladder[r].r_level, close_volume);

            if(m_move_to_breakeven_after_first && !m_managed[i].breakeven_done)
              {
               double be_price = m_managed[i].entry_price;
               double tp       = PositionGetDouble(POSITION_TP);
               if(m_trade.PositionModify(ticket, be_price, tp))
                  m_managed[i].breakeven_done = true;
              }
           }
         else
           {
            PrintFormat("LadderExecutor: partial close failed ticket=%I64u rung=%d retcode=%d",
                        ticket, r, m_trade.ResultRetcode());
           }
        }
     }
  }

#endif // LADDEREXECUTOR_MQH
