//+------------------------------------------------------------------+
//|                                               TrailingEngine.mqh |
//+------------------------------------------------------------------+
#ifndef TRAILINGENGINE_MQH
#define TRAILINGENGINE_MQH

#include "ITrailMethod.mqh"

//+------------------------------------------------------------------+
//| CTrailingEngine                                                  |
//+------------------------------------------------------------------+
class CTrailingEngine
  {
private:
   //--- internal entry linking a ticket to its trail method
   struct SEntry
     {
      ulong             ticket;
      ITrailMethod     *method;
      bool              active;
     };

   SEntry            m_entries[];         // the position registry
   int               m_count;             // number of active entries
   int               m_capacity;          // allocated capacity of m_entries
   double            m_min_sl_distance;   // minimum pip distance from price to prevent too-tight SL

   //--- internal helpers
   void              GrowIfNeeded(void);
   int               FindEntry(ulong ticket) const;
   bool              IsImprovement(const double new_sl, const double current_sl,
                                   const long position_type) const;

public:
                     CTrailingEngine(void);
                    ~CTrailingEngine(void);

   //--- configuration
   void              Configure(const double min_sl_distance_pips);

   //--- registry management
   bool              Register(ulong ticket, ITrailMethod *method);
   bool              Deregister(ulong ticket);
   bool              IsRegistered(ulong ticket) const;
   int               Count(void) const;

   //--- engine execution
   void              OnTick(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CTrailingEngine::CTrailingEngine(void) : m_count(0),
   m_capacity(32),
   m_min_sl_distance(0.0)
  {
   ::ArrayResize(m_entries, m_capacity);
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CTrailingEngine::~CTrailingEngine(void)
  {
   ::ArrayFree(m_entries);
  }
//+------------------------------------------------------------------+
//| Configure                                                        |
//| Sets a global minimum pip distance that the proposed SL must     |
//| sit away from the current price. Use 0.0 to disable.             |
//+------------------------------------------------------------------+
void CTrailingEngine::Configure(const double min_sl_distance_pips)
  {
   m_min_sl_distance = (min_sl_distance_pips >= 0.0 ? min_sl_distance_pips : 0.0);
  }
//+------------------------------------------------------------------+
//| GrowIfNeeded                                                     |
//| Doubles the registry capacity when the array is full.            |
//+------------------------------------------------------------------+
void CTrailingEngine::GrowIfNeeded(void)
  {
   if(m_count < m_capacity)
      return;

   m_capacity *= 2;
   ::ArrayResize(m_entries, m_capacity);
  }
//+------------------------------------------------------------------+
//| FindEntry                                                        |
//| Returns the index of the entry with the given ticket, or -1.     |
//+------------------------------------------------------------------+
int CTrailingEngine::FindEntry(ulong ticket) const
  {
   for(int i = 0; i < m_count; i++)
     {
      if(m_entries[i].ticket == ticket)
         return(i);
     }
   return(-1);
  }
//+------------------------------------------------------------------+
//| IsImprovement                                                    |
//| Returns true when new_sl improves SL (higher for longs;          |
//| lower for shorts), or when no SL is set (current_sl==0.0).       |
//+------------------------------------------------------------------+
bool CTrailingEngine::IsImprovement(const double new_sl, const double current_sl,
                                    const long position_type) const
  {
   if(position_type == POSITION_TYPE_BUY)
      return(new_sl > current_sl);
   else
      return(new_sl < current_sl || current_sl == 0.0);
  }
//+------------------------------------------------------------------+
//| IsRegistered                                                     |
//| Returns true if the given ticket is currently in the registry.   |
//+------------------------------------------------------------------+
bool CTrailingEngine::IsRegistered(ulong ticket) const
  {
   return(FindEntry(ticket) >= 0);
  }
//+------------------------------------------------------------------+
//| Register                                                         |
//| Associates a position ticket with an ITrailMethod instance.      |
//| The caller retains ownership of the method pointer.              |
//+------------------------------------------------------------------+
bool CTrailingEngine::Register(ulong ticket, ITrailMethod *method)
  {
   if(method == NULL)
      return(false);

//--- replace existing entry if ticket already registered
   int idx = FindEntry(ticket);
   if(idx >= 0)
     {
      m_entries[idx].method = method;
      m_entries[idx].active = true;
      return(true);
     }

   GrowIfNeeded();

   m_entries[m_count].ticket = ticket;
   m_entries[m_count].method = method;
   m_entries[m_count].active = true;
   m_count++;
   return(true);
  }
//+------------------------------------------------------------------+
//| Deregister                                                       |
//| Removes a ticket from the registry. Returns true if found.       |
//+------------------------------------------------------------------+
bool CTrailingEngine::Deregister(ulong ticket)
  {
   int idx = FindEntry(ticket);
   if(idx < 0)
      return(false);

//--- compact the array by overwriting with the last entry
   m_count--;
   if(idx < m_count)
      m_entries[idx] = m_entries[m_count];

   return(true);
  }
//+------------------------------------------------------------------+
//| Count                                                            |
//| Returns the number of currently registered positions.            |
//+------------------------------------------------------------------+
int CTrailingEngine::Count(void) const
  {
   return(m_count);
  }
//+------------------------------------------------------------------+
//| OnTick                                                           |
//+------------------------------------------------------------------+
void CTrailingEngine::OnTick(void)
  {
   for(int i = m_count - 1; i >= 0; i--)
     {
      if(!m_entries[i].active || m_entries[i].method == NULL)
         continue;

      ulong ticket = m_entries[i].ticket;

      //--- verify the position still exists; deregister if closed
      if(!::PositionSelectByTicket(ticket))
        {
         Deregister(ticket);
         continue;
        }

      double current_sl    = ::PositionGetDouble(POSITION_SL);
      long   position_type = ::PositionGetInteger(POSITION_TYPE);
      string symbol        = ::PositionGetString(POSITION_SYMBOL);
      int    digits        = (int)::SymbolInfoInteger(symbol, SYMBOL_DIGITS);
      double point_size    = ::SymbolInfoDouble(symbol, SYMBOL_POINT);

      //--- call interface method on pointer
      double proposed_sl = m_entries[i].method.ComputeStopLevel(ticket);

      //--- method returned 0.0 meaning it cannot produce a level yet
      if(proposed_sl <= 0.0)
         continue;

      //--- apply the strict improvement check
      if(!IsImprovement(proposed_sl, current_sl, position_type))
         continue;

      //--- skip if the difference is smaller than one point; the broker
      //--- rounds sub-point differences to zero and returns retcode 10025
      if(::MathAbs(proposed_sl - current_sl) < point_size)
         continue;

      double current_tp = ::PositionGetDouble(POSITION_TP);

      //--- log the modification with old SL, new SL, method name, and pip improvement
      string method_name = m_entries[i].method.MethodName();
      double pip_size    = (digits == 3 || digits == 5) ? point_size * 10.0 : point_size;
      double improvement = ::MathAbs(proposed_sl - current_sl) / pip_size;

      ::PrintFormat("CTrailingEngine: ticket=%llu method=%s SL %.5f -> %.5f (+%.1f pips)",
                    ticket, method_name, current_sl, proposed_sl, improvement);

      //--- submit the modification
      MqlTradeRequest req;
      MqlTradeResult  res;
      ::ZeroMemory(req);
      ::ZeroMemory(res);

      req.action   = TRADE_ACTION_SLTP;
      req.position = ticket;
      req.symbol   = symbol;
      req.sl       = proposed_sl;
      req.tp       = current_tp;

      if(!::OrderSend(req, res))
         ::PrintFormat("CTrailingEngine: PositionModify failed for ticket=%llu retcode=%d",
                       ticket, res.retcode);
     }
  }

#endif // TRAILINGENGINE_MQH
//+------------------------------------------------------------------+