//+------------------------------------------------------------------+
//|                                           PartialCloseEngine.mqh |
//+------------------------------------------------------------------+
#ifndef PARTIALCLOSEENGINE_MQH
#define PARTIALCLOSEENGINE_MQH

#include "LadderLevel.mqh"
#include "PositionRecord.mqh"
#include "VolumeNormalizer.mqh"
#include "PartialCloseExecutor.mqh"
#include "BreakevenManager.mqh"
#include "ChartLevelDrawer.mqh"

#define PCE_MAX_POSITIONS 32

//+------------------------------------------------------------------+
//| CPartialCloseEngine                                              |
//| Public entry point for the whole partial close system. It owns   |
//| one instance of each sub-component and coordinates them through  |
//| Register(), OnTick(), and Deregister(). The engine itself never  |
//| touches OrderSend() or PositionModify() directly; every trade    |
//| action is delegated to CPartialCloseExecutor or                  |
//| CBreakevenManager so those two classes remain the single place   |
//| where trade requests are built.                                  |
//+------------------------------------------------------------------+
class CPartialCloseEngine
  {
private:
   long                    m_chart_id;
   CVolumeNormalizer       m_normalizer;
   CPartialCloseExecutor   m_executor;
   CBreakevenManager       m_breakeven;
   CChartLevelDrawer       m_drawer;
   CPositionRecord         m_positions[PCE_MAX_POSITIONS];
   int                     m_position_count;

   int               FindPositionIndex(const ulong ticket) const;
   double            TriggerPrice(const CPositionRecord &record, const double r_multiple) const;
   bool              LevelReached(const CPositionRecord &record,
                                  const double trigger_price,
                                  const double current_price) const;

public:
                     CPartialCloseEngine(void);
                    ~CPartialCloseEngine(void);

   void              SetChartId(const long chart_id) { m_chart_id = chart_id; }

   bool              Register(const ulong ticket,
                              const CLadderLevel &levels[],
                              const int level_count);
   void              Deregister(const ulong ticket);
   void              OnTick(void);
   string            GetStatus(void);
   int               PositionCount(void) const { return(m_position_count); }
  };

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CPartialCloseEngine::CPartialCloseEngine(void)
  {
   m_chart_id       = 0;
   m_position_count = 0;
  }

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CPartialCloseEngine::~CPartialCloseEngine(void)
  {
  }

//+------------------------------------------------------------------+
//| FindPositionIndex                                                |
//| Finds the internal array index of a registered position, or -1   |
//| if the ticket is not currently registered.                       |
//+------------------------------------------------------------------+
int CPartialCloseEngine::FindPositionIndex(const ulong ticket) const
  {
   for(int i = 0; i < m_position_count; i++)
     {
      if(m_positions[i].Ticket() == ticket)
         return(i);
     }
   return(-1);
  }

//+------------------------------------------------------------------+
//| TriggerPrice                                                     |
//| Computes the trigger price for a given R-multiple using the      |
//| original entry price and original R stored at registration time. |
//| entry + r_multiple * R for a long, entry - r_multiple * R for a  |
//| short.                                                           |
//+------------------------------------------------------------------+
double CPartialCloseEngine::TriggerPrice(const CPositionRecord &record, const double r_multiple) const
  {
   if(record.Type() == POSITION_TYPE_BUY)
      return(record.OriginalEntryPrice() + r_multiple * record.OriginalR());
   return(record.OriginalEntryPrice() - r_multiple * record.OriginalR());
  }

//+------------------------------------------------------------------+
//| LevelReached                                                     |
//| Returns true once the current market price has reached or passed |
//| the given trigger price, direction-aware for longs and shorts.   |
//+------------------------------------------------------------------+
bool CPartialCloseEngine::LevelReached(const CPositionRecord &record,
                                       const double trigger_price,
                                       const double current_price) const
  {
   if(record.Type() == POSITION_TYPE_BUY)
      return(current_price >= trigger_price);
   return(current_price <= trigger_price);
  }

//+------------------------------------------------------------------+
//| Register                                                         |
//| Registers a position with the engine. The original entry price,  |
//| original stop loss, and original R are read from the live        |
//| position once, here, and never again; this is what keeps every   |
//| later ladder calculation anchored to the trade's true starting   |
//| conditions. Chart lines for every ladder level are drawn         |
//| immediately after the record is stored.                          |
//+------------------------------------------------------------------+
bool CPartialCloseEngine::Register(const ulong ticket,
                                   const CLadderLevel &levels[],
                                   const int level_count)
  {
   if(m_position_count >= PCE_MAX_POSITIONS)
     {
      ::PrintFormat("CPartialCloseEngine: cannot register ticket=%I64u, position table full",
                    ticket);
      return(false);
     }

   if(!::PositionSelectByTicket(ticket))
     {
      ::PrintFormat("CPartialCloseEngine: cannot register ticket=%I64u, position not found",
                    ticket);
      return(false);
     }

   string symbol         = ::PositionGetString(POSITION_SYMBOL);
   long   type           = ::PositionGetInteger(POSITION_TYPE);
   double volume         = ::PositionGetDouble(POSITION_VOLUME);
   double entry_price    = ::PositionGetDouble(POSITION_PRICE_OPEN);
   double sl              = ::PositionGetDouble(POSITION_SL);

   if(sl <= 0.0)
     {
      ::PrintFormat("CPartialCloseEngine: ticket=%I64u has no stop loss, cannot derive R, "
                    "registration refused", ticket);
      return(false);
     }

   double r = (type == POSITION_TYPE_BUY) ? (entry_price - sl) : (sl - entry_price);
   if(r <= 0.0)
     {
      ::PrintFormat("CPartialCloseEngine: ticket=%I64u has a nonsensical stop (r<=0), "
                    "registration refused", ticket);
      return(false);
     }

   int index = m_position_count;
   m_positions[index].Init(ticket, symbol, type, volume, entry_price, sl, r);

   for(int i = 0; i < level_count; i++)
      m_positions[index].AddLevel(levels[i]);

   m_position_count++;

//--- draw one dashed line per ladder level
   for(int i = 0; i < m_positions[index].LevelCount(); i++)
     {
      CLadderLevel *level = m_positions[index].Level(i);
      double trigger = TriggerPrice(m_positions[index], level.RMultiple());
      string obj_name = m_drawer.DrawLevel(m_chart_id, ticket, level.RMultiple(),
                                           level.ClosePct(), trigger, clrDodgerBlue);
      if(obj_name != "")
         m_positions[index].AddObjectName(obj_name);
     }

   ::PrintFormat("CPartialCloseEngine: registered ticket=%I64u symbol=%s type=%s "
                 "volume=%.2f entry=%.5f sl=%.5f R=%.5f levels=%d",
                 ticket, symbol, (type == POSITION_TYPE_BUY ? "buy" : "sell"),
                 volume, entry_price, sl, r, level_count);

   return(true);
  }

//+------------------------------------------------------------------+
//| Deregister                                                       |
//| Removes a position from the engine and deletes every chart       |
//| object that was drawn for it.                                    |
//+------------------------------------------------------------------+
void CPartialCloseEngine::Deregister(const ulong ticket)
  {
   int index = FindPositionIndex(ticket);
   if(index < 0)
      return;

   m_drawer.RemoveAll(m_chart_id, ticket);

//--- compact the array by shifting everything after index left by one
   for(int i = index; i < m_position_count - 1; i++)
      m_positions[i] = m_positions[i + 1];

   m_position_count--;

   ::PrintFormat("CPartialCloseEngine: deregistered ticket=%I64u", ticket);
  }

//+------------------------------------------------------------------+
//| OnTick                                                           |
//| Called on every tick. Iterates all registered positions and      |
//| evaluates every unvisited ladder level against the current       |
//| market price. When a level triggers, the close volume is         |
//| computed from the ORIGINAL entry volume (never the live volume), |
//| clamped for remainder safety, sent through the executor, and,    |
//| if confirmed and the level's breakeven flag is set, the stop is  |
//| moved through the breakeven manager.                             |
//+------------------------------------------------------------------+
void CPartialCloseEngine::OnTick(void)
  {
   for(int i = m_position_count - 1; i >= 0; i--)
     {
      ulong ticket = m_positions[i].Ticket();

      if(!::PositionSelectByTicket(ticket))
        {
         //--- position closed outside the engine, clean up and move on
         Deregister(ticket);
         continue;
        }

      string symbol       = m_positions[i].Symbol();
      double current_price = (m_positions[i].Type() == POSITION_TYPE_BUY)
                             ? ::SymbolInfoDouble(symbol, SYMBOL_BID)
                             : ::SymbolInfoDouble(symbol, SYMBOL_ASK);
      double position_volume = ::PositionGetDouble(POSITION_VOLUME);

      for(int lvl = 0; lvl < m_positions[i].LevelCount(); lvl++)
        {
         CLadderLevel *level = m_positions[i].Level(lvl);
         if(level.Hit())
            continue;

         double trigger = TriggerPrice(m_positions[i], level.RMultiple());
         if(!LevelReached(m_positions[i], trigger, current_price))
            continue;

         //--- percentage always applies to the ORIGINAL volume, not the
         //--- current, already-reduced position volume
         double raw_close = m_positions[i].OriginalVolume() * level.ClosePct() / 100.0;
         double close_volume = m_normalizer.ClampClose(symbol, position_volume, raw_close);

         if(close_volume <= 0.0)
           {
            ::PrintFormat("CPartialCloseEngine: ticket=%I64u level=%.1fR produced no valid "
                          "close volume, marking hit to avoid repeated attempts",
                          ticket, level.RMultiple());
            level.SetHit(true);
            continue;
           }

         ::PrintFormat("CPartialCloseEngine: ticket=%I64u level=%.1fR triggered at price=%.5f "
                       "raw_close=%.2f normalized_close=%.2f",
                       ticket, level.RMultiple(), current_price, raw_close, close_volume);

         bool closed = m_executor.ExecutePartialClose(ticket, symbol,
                       m_positions[i].Type(), close_volume);

         if(!closed)
           {
            //--- leave the level un-hit so the engine retries on the next tick
            continue;
           }

         level.SetHit(true);

         //--- mark the chart line for this level as hit
         for(int obj = 0; obj < m_positions[i].ObjectCount(); obj++)
           {
            string name = m_positions[i].ObjectName(obj);
            if(::StringFind(name, ::StringFormat("%.1fR", level.RMultiple())) >= 0)
               m_drawer.MarkHit(m_chart_id, name, clrGray);
           }

         if(level.MoveToBreakeven())
           {
            double point = ::SymbolInfoDouble(symbol, SYMBOL_POINT);
            double current_tp = ::PositionGetDouble(POSITION_TP);
            m_breakeven.MoveToBreakeven(ticket, symbol, m_positions[i].Type(),
                                        m_positions[i].OriginalEntryPrice(), point, current_tp);
           }

         //--- refresh the live volume before evaluating the next level
         if(::PositionSelectByTicket(ticket))
            position_volume = ::PositionGetDouble(POSITION_VOLUME);
        }
     }
  }

//+------------------------------------------------------------------+
//| GetStatus                                                        |
//| Returns a short human-readable status string listing every       |
//| registered ticket and how many ladder levels remain unhit.       |
//+------------------------------------------------------------------+
string CPartialCloseEngine::GetStatus(void)
  {
   string status = ::StringFormat("CPartialCloseEngine: %d position(s) registered\n",
                                  m_position_count);
   for(int i = 0; i < m_position_count; i++)
     {
      int remaining = 0;
      for(int lvl = 0; lvl < m_positions[i].LevelCount(); lvl++)
        {
         if(!m_positions[i].Level(lvl).Hit())
            remaining++;
        }
      status += ::StringFormat("  ticket=%I64u symbol=%s levels_remaining=%d/%d\n",
                               m_positions[i].Ticket(), m_positions[i].Symbol(),
                               remaining, m_positions[i].LevelCount());
     }
   return(status);
  }

#endif // PARTIALCLOSEENGINE_MQH
//+------------------------------------------------------------------+