//+------------------------------------------------------------------+
//| OrderSplitting_Simple.mqh                                        |
//| Single-symbol MQL5 order splitting module                        |
//| Version 1.1                                                      |
//+------------------------------------------------------------------+
#property strict

#ifndef ORDER_SPLITTING_SIMPLE_MQH
#define ORDER_SPLITTING_SIMPLE_MQH

input string OS_Header              = "----------- Order Splitting -----------";
input bool   OS_UseSplitEntry       = false;  // Enable split entries
input int    OS_SplitCount          = 2;      // Total positions, 1-10
input int    OS_SplitDelaySeconds   = 10;     // Delay between split entries/closes
input double OS_SplitSlippagePoints = 5.0;    // Deviation in points

input string OS_EODHeader           = "----------- Split EOD Close -----------";
input bool   OS_UseSplitEODClose    = false;  // Enable timed split closes
input string OS_EODCloseTimeSplits  = "19:45"; // Close positions 2-N
input string OS_EODCloseTimePos1    = "19:50"; // Close position 1

input int    OS_MagicNumber         = 11111;

#define OS_MAX_SPLITS 10

ulong    osLongTickets[OS_MAX_SPLITS];
ulong    osShortTickets[OS_MAX_SPLITS];
int      osLongNextEntry       = 0;
int      osShortNextEntry      = 0;
int      osLongNextClose       = 0;
int      osShortNextClose      = 0;
datetime osLongNextFireTime    = 0;
datetime osShortNextFireTime   = 0;
datetime osLongCloseFireTime   = 0;
datetime osShortCloseFireTime  = 0;
bool     osLongPos1Closed      = false;
bool     osShortPos1Closed     = false;
bool     osLongTPChain         = false;
bool     osShortTPChain        = false;
double   osLongSL              = 0.0;
double   osShortSL             = 0.0;
double   osLongLots            = 0.0;
double   osShortLots           = 0.0;

datetime osLastEODDate         = 0;
bool     osEODSplitsActive     = false;
int      osEODNextClose        = 1;
datetime osEODFireTime         = 0;

int OS_SplitLimit()
{
   return MathMax(1, MathMin(OS_MAX_SPLITS, OS_SplitCount));
}

double OS_NormalizeVolume(const string symbol, const double lots)
{
   double minLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
   double maxLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
   double step   = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);

   if(step <= 0.0) step = 0.01;
   if(minLot <= 0.0) minLot = step;
   if(maxLot <= 0.0) maxLot = lots;

   double aligned = MathFloor(lots / step + 1e-8) * step;
   aligned = MathMax(minLot, MathMin(maxLot, aligned));

   int digits = 0;
   double tmp = step;
   while(digits < 8 && MathAbs(tmp - MathRound(tmp)) > 1e-8)
   {
      tmp *= 10.0;
      digits++;
   }

   return NormalizeDouble(aligned, digits);
}

ENUM_ORDER_TYPE_FILLING OS_FillingMode(const string symbol)
{
   long filling = SymbolInfoInteger(symbol, SYMBOL_FILLING_MODE);

   if((filling & SYMBOL_FILLING_FOK) == SYMBOL_FILLING_FOK)
      return ORDER_FILLING_FOK;
   if((filling & SYMBOL_FILLING_IOC) == SYMBOL_FILLING_IOC)
      return ORDER_FILLING_IOC;

   return ORDER_FILLING_RETURN;
}

bool OS_RetcodeOK(const uint retcode)
{
   return retcode == TRADE_RETCODE_DONE ||
          retcode == TRADE_RETCODE_DONE_PARTIAL ||
          retcode == TRADE_RETCODE_PLACED;
}

datetime OS_Today()
{
   MqlDateTime t;
   TimeToStruct(TimeCurrent(), t);
   return StringToTime(StringFormat("%04d.%02d.%02d", t.year, t.mon, t.day));
}

bool OS_TimeWindowHit(const string timeText, const int windowSeconds = 60)
{
   if(timeText == "") return false;

   datetime now = TimeCurrent();
   datetime target = StringToTime(TimeToString(now, TIME_DATE) + " " + timeText);
   return target > 0 && now >= target && now < target + windowSeconds;
}

bool OS_SplitHitSL(const ulong positionId)
{
   if(!HistorySelect(TimeCurrent() - 14 * 86400, TimeCurrent()))
      return false;

   for(int i = HistoryDealsTotal() - 1; i >= 0; i--)
   {
      ulong deal = HistoryDealGetTicket(i);
      if((ulong)HistoryDealGetInteger(deal, DEAL_POSITION_ID) != positionId)
         continue;
      if(HistoryDealGetInteger(deal, DEAL_ENTRY) != DEAL_ENTRY_OUT)
         continue;

      return (ENUM_DEAL_REASON)HistoryDealGetInteger(deal, DEAL_REASON) == DEAL_REASON_SL;
   }

   return false;
}

bool OS_FindNewestPosition(const ENUM_ORDER_TYPE type, const datetime afterTime, ulong &ticket)
{
   ticket = 0;
   datetime newest = 0;

   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong posTicket = PositionGetTicket(i);
      if(posTicket == 0 || !PositionSelectByTicket(posTicket))
         continue;

      if(PositionGetString(POSITION_SYMBOL) != _Symbol)
         continue;
      if((int)PositionGetInteger(POSITION_MAGIC) != OS_MagicNumber)
         continue;

      ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      if(type == ORDER_TYPE_BUY && posType != POSITION_TYPE_BUY)
         continue;
      if(type == ORDER_TYPE_SELL && posType != POSITION_TYPE_SELL)
         continue;

      datetime openTime = (datetime)PositionGetInteger(POSITION_TIME);
      if(afterTime > 0 && openTime + 3 < afterTime)
         continue;

      if(openTime >= newest)
      {
         newest = openTime;
         ticket = posTicket;
      }
   }

   return ticket > 0;
}

void OS_ResetLong()
{
   ArrayInitialize(osLongTickets, 0);
   osLongNextEntry = 0;
   osLongNextClose = 0;
   osLongNextFireTime = 0;
   osLongCloseFireTime = 0;
   osLongPos1Closed = false;
   osLongTPChain = false;
   osLongSL = 0.0;
   osLongLots = 0.0;
}

void OS_ResetShort()
{
   ArrayInitialize(osShortTickets, 0);
   osShortNextEntry = 0;
   osShortNextClose = 0;
   osShortNextFireTime = 0;
   osShortCloseFireTime = 0;
   osShortPos1Closed = false;
   osShortTPChain = false;
   osShortSL = 0.0;
   osShortLots = 0.0;
}

void OS_Init()
{
   OS_ResetLong();
   OS_ResetShort();
}

double OS_AdjustLots(const double fullLots)
{
   if(!OS_UseSplitEntry) return OS_NormalizeVolume(_Symbol, fullLots);

   int splitCount = OS_SplitLimit();
   if(splitCount <= 1) return OS_NormalizeVolume(_Symbol, fullLots);

   double lots = OS_NormalizeVolume(_Symbol, fullLots / splitCount);
   if(lots <= 0.0)
      Print("OS: split lot size is invalid after alignment. fullLots=", fullLots, " splitCount=", splitCount);

   return lots;
}

bool OS_CloseTicket(const ulong ticket)
{
   if(ticket == 0 || !PositionSelectByTicket(ticket))
      return false;

   string symbol = PositionGetString(POSITION_SYMBOL);
   ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);

   MqlTradeRequest req = {};
   MqlTradeResult  res = {};

   req.action       = TRADE_ACTION_DEAL;
   req.position     = ticket;
   req.symbol       = symbol;
   req.volume       = PositionGetDouble(POSITION_VOLUME);
   req.type         = (posType == POSITION_TYPE_BUY) ? ORDER_TYPE_SELL : ORDER_TYPE_BUY;
   req.price        = (req.type == ORDER_TYPE_SELL) ? SymbolInfoDouble(symbol, SYMBOL_BID) : SymbolInfoDouble(symbol, SYMBOL_ASK);
   req.deviation    = (ulong)MathMax(0.0, OS_SplitSlippagePoints);
   req.magic        = OS_MagicNumber;
   req.type_filling = OS_FillingMode(symbol);

   bool sent = OrderSend(req, res);
   if(!sent || !OS_RetcodeOK(res.retcode))
   {
      Print("OS: close failed ticket=", ticket, " retcode=", res.retcode, " comment=", res.comment);
      return false;
   }

   if(PositionSelectByTicket(ticket))
   {
      Print("OS: close incomplete ticket=", ticket, " remaining=", PositionGetDouble(POSITION_VOLUME), " retcode=", res.retcode);
      return false;
   }

   return true;
}

bool OS_OpenSplitMarket(const ENUM_ORDER_TYPE type, const double sl, const double lots, ulong &outTicket)
{
   outTicket = 0;
   string symbol = _Symbol;
   double volume = OS_NormalizeVolume(symbol, lots);

   if(volume <= 0.0)
   {
      Print("OS: open skipped, invalid split volume=", volume);
      return false;
   }

   MqlTradeRequest req = {};
   MqlTradeResult  res = {};

   req.action       = TRADE_ACTION_DEAL;
   req.symbol       = symbol;
   req.volume       = volume;
   req.type         = type;
   req.price        = (type == ORDER_TYPE_BUY) ? SymbolInfoDouble(symbol, SYMBOL_ASK) : SymbolInfoDouble(symbol, SYMBOL_BID);
   req.sl           = (sl > 0.0) ? NormalizeDouble(sl, _Digits) : 0.0;
   req.tp           = 0.0;
   req.deviation    = (ulong)MathMax(0.0, OS_SplitSlippagePoints);
   req.magic        = OS_MagicNumber;
   req.type_filling = OS_FillingMode(symbol);

   datetime beforeSend = TimeCurrent();
   bool sent = OrderSend(req, res);
   if(!sent || !OS_RetcodeOK(res.retcode))
   {
      Print("OS: split open failed type=", EnumToString(type), " retcode=", res.retcode, " comment=", res.comment);
      return false;
   }

   outTicket = res.order;
   if(outTicket > 0 && PositionSelectByTicket(outTicket))
      return true;

   return OS_FindNewestPosition(type, beforeSend, outTicket);
}

void OS_RegisterLong(const ulong positionTicket)
{
   int splitCount = OS_SplitLimit();
   if(!OS_UseSplitEntry || splitCount <= 1 || positionTicket == 0)
      return;

   if(!PositionSelectByTicket(positionTicket))
   {
      Print("OS: cannot register long, position not found ticket=", positionTicket);
      return;
   }

   OS_ResetLong();
   osLongTickets[0] = positionTicket;
   osLongNextEntry = 1;
   osLongNextClose = 1;
   osLongSL = PositionGetDouble(POSITION_SL);
   osLongLots = PositionGetDouble(POSITION_VOLUME);
   osLongNextFireTime = TimeCurrent() + MathMax(0, OS_SplitDelaySeconds);
}

void OS_RegisterShort(const ulong positionTicket)
{
   int splitCount = OS_SplitLimit();
   if(!OS_UseSplitEntry || splitCount <= 1 || positionTicket == 0)
      return;

   if(!PositionSelectByTicket(positionTicket))
   {
      Print("OS: cannot register short, position not found ticket=", positionTicket);
      return;
   }

   OS_ResetShort();
   osShortTickets[0] = positionTicket;
   osShortNextEntry = 1;
   osShortNextClose = 1;
   osShortSL = PositionGetDouble(POSITION_SL);
   osShortLots = PositionGetDouble(POSITION_VOLUME);
   osShortNextFireTime = TimeCurrent() + MathMax(0, OS_SplitDelaySeconds);
}

void OS_ManageLong()
{
   datetime now = TimeCurrent();
   int splitCount = OS_SplitLimit();

   if(osLongTickets[0] > 0 && !osLongPos1Closed && osLongNextEntry > 0 && osLongNextEntry < splitCount && now >= osLongNextFireTime)
   {
      ulong ticket = 0;
      if(OS_OpenSplitMarket(ORDER_TYPE_BUY, osLongSL, osLongLots, ticket))
      {
         osLongTickets[osLongNextEntry] = ticket;
         Print("OS: split long pos", osLongNextEntry, " opened ticket=", ticket);
         osLongNextEntry++;
         osLongNextFireTime = (osLongNextEntry < splitCount) ? now + MathMax(0, OS_SplitDelaySeconds) : 0;
      }
   }

   if(osLongTickets[0] > 0 && !osLongPos1Closed && !PositionSelectByTicket(osLongTickets[0]))
   {
      bool hitSL = OS_SplitHitSL(osLongTickets[0]);
      Print("OS: long pos0 closed sl=", hitSL);

      if(hitSL)
      {
         OS_ResetLong();
         return;
      }

      osLongNextEntry = splitCount;
      osLongPos1Closed = true;
      osLongTickets[0] = 0;

      if(splitCount > 1)
      {
         osLongTPChain = true;
         osLongNextClose = 1;
         osLongCloseFireTime = now + MathMax(0, OS_SplitDelaySeconds);
      }
   }

   if(osLongTPChain && osLongNextClose < splitCount && now >= osLongCloseFireTime)
   {
      ulong ticket = osLongTickets[osLongNextClose];
      if(ticket == 0 || OS_CloseTicket(ticket))
      {
         osLongTickets[osLongNextClose] = 0;
         osLongNextClose++;

         if(osLongNextClose >= splitCount)
         {
            OS_ResetLong();
         }
         else
         {
            osLongCloseFireTime = now + MathMax(0, OS_SplitDelaySeconds);
         }
      }
      else
      {
         osLongCloseFireTime = now + MathMax(1, OS_SplitDelaySeconds);
      }
   }
}

void OS_ManageShort()
{
   datetime now = TimeCurrent();
   int splitCount = OS_SplitLimit();

   if(osShortTickets[0] > 0 && !osShortPos1Closed && osShortNextEntry > 0 && osShortNextEntry < splitCount && now >= osShortNextFireTime)
   {
      ulong ticket = 0;
      if(OS_OpenSplitMarket(ORDER_TYPE_SELL, osShortSL, osShortLots, ticket))
      {
         osShortTickets[osShortNextEntry] = ticket;
         Print("OS: split short pos", osShortNextEntry, " opened ticket=", ticket);
         osShortNextEntry++;
         osShortNextFireTime = (osShortNextEntry < splitCount) ? now + MathMax(0, OS_SplitDelaySeconds) : 0;
      }
   }

   if(osShortTickets[0] > 0 && !osShortPos1Closed && !PositionSelectByTicket(osShortTickets[0]))
   {
      bool hitSL = OS_SplitHitSL(osShortTickets[0]);
      Print("OS: short pos0 closed sl=", hitSL);

      if(hitSL)
      {
         OS_ResetShort();
         return;
      }

      osShortNextEntry = splitCount;
      osShortPos1Closed = true;
      osShortTickets[0] = 0;

      if(splitCount > 1)
      {
         osShortTPChain = true;
         osShortNextClose = 1;
         osShortCloseFireTime = now + MathMax(0, OS_SplitDelaySeconds);
      }
   }

   if(osShortTPChain && osShortNextClose < splitCount && now >= osShortCloseFireTime)
   {
      ulong ticket = osShortTickets[osShortNextClose];
      if(ticket == 0 || OS_CloseTicket(ticket))
      {
         osShortTickets[osShortNextClose] = 0;
         osShortNextClose++;

         if(osShortNextClose >= splitCount)
         {
            OS_ResetShort();
         }
         else
         {
            osShortCloseFireTime = now + MathMax(0, OS_SplitDelaySeconds);
         }
      }
      else
      {
         osShortCloseFireTime = now + MathMax(1, OS_SplitDelaySeconds);
      }
   }
}

void OS_ManageEOD()
{
   if(!OS_UseSplitEODClose) return;

   datetime now = TimeCurrent();
   datetime today = OS_Today();
   int splitCount = OS_SplitLimit();

   if(!osEODSplitsActive && osLastEODDate != today && OS_TimeWindowHit(OS_EODCloseTimeSplits))
   {
      osEODSplitsActive = true;
      osEODNextClose = 1;
      osEODFireTime = now;
   }

   if(osEODSplitsActive && osEODNextClose < splitCount && now >= osEODFireTime)
   {
      bool longClosed = osLongTickets[osEODNextClose] == 0 || OS_CloseTicket(osLongTickets[osEODNextClose]);
      bool shortClosed = osShortTickets[osEODNextClose] == 0 || OS_CloseTicket(osShortTickets[osEODNextClose]);

      if(longClosed)
         osLongTickets[osEODNextClose] = 0;
      if(shortClosed)
         osShortTickets[osEODNextClose] = 0;

      if(longClosed && shortClosed)
      {
         osEODNextClose++;

         if(osEODNextClose >= splitCount)
            osEODSplitsActive = false;
         else
            osEODFireTime = now + MathMax(0, OS_SplitDelaySeconds);
      }
      else
      {
         osEODFireTime = now + MathMax(1, OS_SplitDelaySeconds);
      }
   }

   if(osLastEODDate != today && OS_TimeWindowHit(OS_EODCloseTimePos1))
   {
      bool longClosed = osLongTickets[0] == 0 || OS_CloseTicket(osLongTickets[0]);
      bool shortClosed = osShortTickets[0] == 0 || OS_CloseTicket(osShortTickets[0]);

      if(longClosed && shortClosed)
      {
         OS_ResetLong();
         OS_ResetShort();
         osLastEODDate = today;
      }
   }
}

void OS_Manage()
{
   if(OS_UseSplitEntry)
   {
      OS_ManageLong();
      OS_ManageShort();
   }

   OS_ManageEOD();
}

#endif
