//+------------------------------------------------------------------+
//|                                                BasketScanner.mqh |
//+------------------------------------------------------------------+
#ifndef BASKETSCANNER_MQH
#define BASKETSCANNER_MQH

#include "BasketInfo.mqh"

//+------------------------------------------------------------------+
//| CBasketScanner                                                   |
//| Responsible solely for reading open positions and grouping them  |
//| by basket ID. Performs no trading operations. The basket ID is   |
//| extracted from POSITION_COMMENT by scanning for the "BASKET:"    |
//| prefix and taking the remainder as the ID.                       |
//+------------------------------------------------------------------+
class CBasketScanner
  {
private:
   string            m_prefix;            // the comment prefix used to mark basket legs

   string            ExtractBasketId(const string comment) const;

public:
                     CBasketScanner(void);
                    ~CBasketScanner(void);

   //--- basket query operations
   bool              GetBasketInfo(const string basket_id, CBasketInfo &info) const;
   int               GetAllBaskets(CBasketInfo &baskets[]) const;
   bool              IsBasketLeg(const string comment) const;
  };

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CBasketScanner::CBasketScanner(void) : m_prefix("BASKET:")
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CBasketScanner::~CBasketScanner(void)
  {
  }
//+------------------------------------------------------------------+
//| ExtractBasketId                                                  |
//| Finds the "BASKET:" prefix in comment and returns the substring  |
//| after it. Returns an empty string if the prefix is absent or if  |
//| the ID portion is empty.                                         |
//+------------------------------------------------------------------+
string CBasketScanner::ExtractBasketId(const string comment) const
  {
   int pos = ::StringFind(comment, m_prefix);
   if(pos < 0)
      return("");

   int id_start = pos + ::StringLen(m_prefix);
   string id    = ::StringSubstr(comment, id_start);
   return(id);
  }
//+------------------------------------------------------------------+
//| IsBasketLeg                                                      |
//+------------------------------------------------------------------+
bool CBasketScanner::IsBasketLeg(const string comment) const
  {
   return(::StringFind(comment, m_prefix) >= 0);
  }
//+------------------------------------------------------------------+
//| GetBasketInfo                                                    |
//| Scans all open positions, collects those matching basket_id,     |
//| and populates the info struct. Returns false if none found.      |
//+------------------------------------------------------------------+
bool CBasketScanner::GetBasketInfo(const string basket_id, CBasketInfo &info) const
  {
   info.basket_id          = basket_id;
   info.legs               = 0;
   info.total_long_volume  = 0.0;
   info.total_short_volume = 0.0;
   info.aggregate_pnl      = 0.0;
   info.vw_pips            = 0.0;
   info.last_scan          = ::TimeCurrent();

   double weighted_pip_sum = 0.0;
   double total_volume     = 0.0;

   int total = ::PositionsTotal();

   for(int i = 0; i < total; i++)
     {
      ulong ticket = ::PositionGetTicket(i);
      if(ticket == 0)
         continue;

      if(!::PositionSelectByTicket(ticket))
         continue;

      string comment = ::PositionGetString(POSITION_COMMENT);
      if(ExtractBasketId(comment) != basket_id)
         continue;

      //--- this position belongs to the basket
      long   pos_type  = ::PositionGetInteger(POSITION_TYPE);
      double volume    = ::PositionGetDouble(POSITION_VOLUME);
      double pnl       = ::PositionGetDouble(POSITION_PROFIT);
      double open_px   = ::PositionGetDouble(POSITION_PRICE_OPEN);
      double cur_px    = ::PositionGetDouble(POSITION_PRICE_CURRENT);
      string symbol    = ::PositionGetString(POSITION_SYMBOL);
      double point     = ::SymbolInfoDouble(symbol, SYMBOL_POINT);
      int    digits    = (int)::SymbolInfoInteger(symbol, SYMBOL_DIGITS);
      double pip_size  = (digits == 3 || digits == 5) ? point * 10.0 : point;

      //--- compute pip P&L for this leg
      double pip_pnl = 0.0;
      if(pip_size > 0.0)
        {
         if(pos_type == POSITION_TYPE_BUY)
            pip_pnl = (cur_px - open_px) / pip_size;
         else
            pip_pnl = (open_px - cur_px) / pip_size;
        }

      info.legs++;
      info.aggregate_pnl += pnl;

      if(pos_type == POSITION_TYPE_BUY)
         info.total_long_volume += volume;
      else
         info.total_short_volume += volume;

      weighted_pip_sum += volume * pip_pnl;
      total_volume     += volume;
     }

   if(info.legs == 0)
      return(false);

   if(total_volume > 0.0)
      info.vw_pips = weighted_pip_sum / total_volume;

   info.distance_to_stop = info.aggregate_pnl - info.stop_threshold;

   return(true);
  }
//+------------------------------------------------------------------+
//| GetAllBaskets                                                    |
//| Returns an array of CBasketInfo structs, one per distinct        |
//| basket ID currently active in the terminal.                      |
//+------------------------------------------------------------------+
int CBasketScanner::GetAllBaskets(CBasketInfo &baskets[]) const
  {
   string found_ids[];
   int    id_count = 0;
   int    total    = ::PositionsTotal();

   for(int i = 0; i < total; i++)
     {
      ulong ticket = ::PositionGetTicket(i);
      if(ticket == 0)
         continue;

      if(!::PositionSelectByTicket(ticket))
         continue;

      string comment = ::PositionGetString(POSITION_COMMENT);
      string id      = ExtractBasketId(comment);
      if(::StringLen(id) == 0)
         continue;

      //--- check if this ID is already in our found list
      bool already = false;
      for(int j = 0; j < id_count; j++)
        {
         if(found_ids[j] == id)
           {
            already = true;
            break;
           }
        }

      if(!already)
        {
         ::ArrayResize(found_ids, id_count + 1);
         found_ids[id_count] = id;
         id_count++;
        }
     }

   ::ArrayResize(baskets, id_count);

   for(int i = 0; i < id_count; i++)
      GetBasketInfo(found_ids[i], baskets[i]);

   return(id_count);
  }

#endif // BASKETSCANNER_MQH
//+------------------------------------------------------------------+