//+------------------------------------------------------------------+
//|                                                    FXP_SessionClose_EA.mq5 |
//|                         FXPIPS ONLINE - https://fxpips.online/   |
//+------------------------------------------------------------------+
#property copyright "FXPIPS ONLINE"
#property link      "https://fxpips.online/mt5-expert-advisors/session-close-ea/"
#property version   "1.00"
#property description "Closes positions and deletes pending orders at a set daily time and"
#property description "before the weekend with a Friday cutoff. Opens no trades."
/*
   MIT License
   Copyright (c) 2026 FXPIPS ONLINE

   Permission is hereby granted, free of charge, to any person obtaining a copy of this
   software and associated documentation files (the "Software"), to deal in the Software
   without restriction, including without limitation the rights to use, copy, modify, merge,
   publish, distribute, sublicense, and/or sell copies of the Software, subject to the
   condition that the above copyright notice and this permission notice are included in all
   copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT
   WARRANTY OF ANY KIND.

   Risk disclaimer: educational tool, not financial advice. Trading forex and CFDs on margin
   carries a substantial risk of loss and is not suitable for every investor. Test on a demo
   account and in the Strategy Tester before using real money. Past behaviour of any rule set
   does not guarantee future results.
*/
#include <Trade/Trade.mqh>

enum ENUM_SYMBOL_SCOPE
  {
   SCOPE_CHART=0,  // Chart symbol only
   SCOPE_ALL=1     // All symbols
  };
enum ENUM_MAGIC_SCOPE
  {
   MAGIC_ANY=0,            // All positions (manual and EAs)
   MAGIC_MANUAL=1,         // Manual positions only (magic 0)
   MAGIC_ONE=2,            // Only the magic number below
   MAGIC_MANUAL_AND_ONE=3  // Manual positions plus the magic number below
  };

input group "Which positions"
input ENUM_SYMBOL_SCOPE InpSymbolScope = SCOPE_ALL;   // Symbols to manage
input ENUM_MAGIC_SCOPE  InpMagicScope  = MAGIC_ANY;  // Positions to manage
input long              InpMagicFilter = 0;          // Magic number used by the filter
input int               InpSlippagePoints = 20;      // Max slippage for closes (points)

input group "Daily close (server time, Monday-Friday)"
input bool InpUseDailyClose     = false;  // Close every trading day at a set time
input int  InpDailyHour         = 22;     // Daily close hour (0-23)
input int  InpDailyMinute       = 45;     // Daily close minute (0-59)
input group "Weekend close (Friday cutoff, server time)"
input bool InpUseFridayCutoff   = true;   // Close before the weekend on Friday
input int  InpFridayHour        = 20;     // Friday cutoff hour (0-23)
input int  InpFridayMinute      = 0;      // Friday cutoff minute (0-59)
input bool InpBlockRestOfFriday = true;   // Keep closing new positions until Friday ends
input group "Actions"
input bool InpClosePositions    = true;   // Close matching positions
input bool InpDeletePendings    = true;   // Delete matching pending orders
input int  InpKeepClosingMinutes= 30;     // After a cutoff, keep closing for this many minutes
input bool InpShowStatus        = true;   // Show the next cutoff on the chart
//+------------------------------------------------------------------+
//| Shared utility helpers                                           |
//+------------------------------------------------------------------+
CTrade trade;
string g_hSym[];
int    g_hHandle[];

bool PassFilter(const string sym,const long magic)
  {
   if(InpSymbolScope==SCOPE_CHART && sym!=_Symbol)
      return(false);
   switch(InpMagicScope)
     {
      case MAGIC_MANUAL:
         return(magic==0);
      case MAGIC_ONE:
         return(magic==InpMagicFilter);
      case MAGIC_MANUAL_AND_ONE:
         return(magic==0 || magic==InpMagicFilter);
      default:
         return(true);
     }
  }

void UtilInit()
  {
   trade.SetDeviationInPoints((ulong)(InpSlippagePoints>0 ? InpSlippagePoints : 0));
   trade.SetTypeFillingBySymbol(_Symbol);
  }

//--- ATR handle per symbol, created on demand
int AtrHandleFor(const string sym,const ENUM_TIMEFRAMES tf,const int period)
  {
   int n=ArraySize(g_hSym);
   for(int i=0; i<n; i++)
      if(g_hSym[i]==sym)
         return(g_hHandle[i]);
   int h=iATR(sym,tf,period);
   if(h==INVALID_HANDLE)
      return(INVALID_HANDLE);
   ArrayResize(g_hSym,n+1);
   ArrayResize(g_hHandle,n+1);
   g_hSym[n]=sym;
   g_hHandle[n]=h;
   return(h);
  }

double AtrFor(const string sym,const ENUM_TIMEFRAMES tf,const int period)
  {
   int h=AtrHandleFor(sym,tf,period);
   if(h==INVALID_HANDLE)
      return(0.0);
   double buf[1];
   if(CopyBuffer(h,0,1,1,buf)!=1)
      return(0.0);
   return(buf[0]);
  }

void ReleaseHandles()
  {
   for(int i=0; i<ArraySize(g_hHandle); i++)
      if(g_hHandle[i]!=INVALID_HANDLE)
         IndicatorRelease(g_hHandle[i]);
   ArrayResize(g_hSym,0);
   ArrayResize(g_hHandle,0);
  }

//--- new SL/TP for a position on any symbol, respecting stops and freeze levels
bool ModifyPosition(const ulong ticket,const string sym,const bool isBuy,double newSL,double newTP,
                    const double curSL,const double curTP,const int minStepPoints)
  {
   double pt=SymbolInfoDouble(sym,SYMBOL_POINT);
   int    dg=(int)SymbolInfoInteger(sym,SYMBOL_DIGITS);
   MqlTick tk;
   if(pt<=0.0 || !SymbolInfoTick(sym,tk))
      return(false);
   double price =isBuy ? tk.bid : tk.ask;
   double stops =(double)SymbolInfoInteger(sym,SYMBOL_TRADE_STOPS_LEVEL)*pt;
   double freeze=(double)SymbolInfoInteger(sym,SYMBOL_TRADE_FREEZE_LEVEL)*pt;
   newSL=(newSL>0.0) ? NormalizeDouble(newSL,dg) : 0.0;
   newTP=(newTP>0.0) ? NormalizeDouble(newTP,dg) : 0.0;
   if(newSL>0.0 && ((isBuy && newSL>=price-stops) || (!isBuy && newSL<=price+stops)))
      return(false);
   if(newTP>0.0 && ((isBuy && newTP<=price+stops) || (!isBuy && newTP>=price-stops)))
      return(false);
   if(freeze>0.0)
     {
      if(curSL>0.0 && MathAbs(price-curSL)<=freeze)
         return(false);
      if(curTP>0.0 && MathAbs(curTP-price)<=freeze)
         return(false);
     }
   bool slChanged=MathAbs(newSL-curSL)>=(minStepPoints>1 ? minStepPoints : 1)*pt;
   bool tpChanged=MathAbs(newTP-curTP)>=pt;
   if(!slChanged && !tpChanged)
      return(false);
   if(!trade.PositionModify(ticket,newSL,newTP))
     {
      PrintFormat("%s #%I64u: modify failed, retcode %u %s",sym,ticket,trade.ResultRetcode(),trade.ResultRetcodeDescription());
      return(false);
     }
   return(true);
  }

bool CloseTicket(const ulong ticket,const string sym,const string reason)
  {
   trade.SetTypeFillingBySymbol(sym);
   if(trade.PositionClose(ticket))
     {
      PrintFormat("%s #%I64u closed (%s).",sym,ticket,reason);
      return(true);
     }
   PrintFormat("%s #%I64u: close failed, retcode %u %s",sym,ticket,trade.ResultRetcode(),trade.ResultRetcodeDescription());
   return(false);
  }

int CloseFiltered(const string reason)
  {
   int closed=0;
   for(int i=PositionsTotal()-1; i>=0; i--)
     {
      ulong ticket=PositionGetTicket(i);
      if(ticket==0)
         continue;
      string sym=PositionGetString(POSITION_SYMBOL);
      if(!PassFilter(sym,PositionGetInteger(POSITION_MAGIC)))
         continue;
      if(CloseTicket(ticket,sym,reason))
         closed++;
     }
   return(closed);
  }

int DeleteFilteredPendings(const string reason)
  {
   int deleted=0;
   for(int i=OrdersTotal()-1; i>=0; i--)
     {
      ulong ticket=OrderGetTicket(i);
      if(ticket==0)
         continue;
      string sym=OrderGetString(ORDER_SYMBOL);
      if(!PassFilter(sym,OrderGetInteger(ORDER_MAGIC)))
         continue;
      if(trade.OrderDelete(ticket))
        {
         PrintFormat("%s pending #%I64u deleted (%s).",sym,ticket,reason);
         deleted++;
        }
      else
         PrintFormat("%s pending #%I64u: delete failed, retcode %u %s",sym,ticket,trade.ResultRetcode(),trade.ResultRetcodeDescription());
     }
   return(deleted);
  }

int CountFiltered()
  {
   int n=0;
   for(int i=PositionsTotal()-1; i>=0; i--)
     {
      ulong ticket=PositionGetTicket(i);
      if(ticket==0)
         continue;
      if(PassFilter(PositionGetString(POSITION_SYMBOL),PositionGetInteger(POSITION_MAGIC)))
         n++;
     }
   return(n);
  }

//+------------------------------------------------------------------+
//| Schedule logic                                                   |
//+------------------------------------------------------------------+
datetime g_lastAttempt=0;

int OnInit()
  {
   if(InpDailyHour<0 || InpDailyHour>23 || InpDailyMinute<0 || InpDailyMinute>59 ||
      InpFridayHour<0 || InpFridayHour>23 || InpFridayMinute<0 || InpFridayMinute>59 || InpKeepClosingMinutes<1)
     {
      Print("Init failed: check the close times.");
      return(INIT_PARAMETERS_INCORRECT);
     }
   if(!InpUseDailyClose && !InpUseFridayCutoff)
     {
      Print("Init failed: enable the daily close and/or the Friday cutoff.");
      return(INIT_PARAMETERS_INCORRECT);
     }
   if(!InpClosePositions && !InpDeletePendings)
     {
      Print("Init failed: enable closing positions and/or deleting pending orders.");
      return(INIT_PARAMETERS_INCORRECT);
     }
   UtilInit();
   EventSetTimer(5);
   Print("Session close started. It never opens trades.");
   return(INIT_SUCCEEDED);
  }

void OnDeinit(const int reason)
  {
   EventKillTimer();
   Comment("");
  }

//--- true while "now" is inside a closing window; reason describes which one
bool InCloseWindow(const datetime now,string &reason)
  {
   MqlDateTime dt;
   TimeToStruct(now,dt);
   int m=dt.hour*60+dt.min;
   if(dt.day_of_week==0 || dt.day_of_week==6)
      return(false);
   if(InpUseFridayCutoff && dt.day_of_week==5)
     {
      int f=InpFridayHour*60+InpFridayMinute;
      if(m>=f && (InpBlockRestOfFriday || m<f+InpKeepClosingMinutes))
        {
         reason="Friday cutoff";
         return(true);
        }
     }
   if(InpUseDailyClose)
     {
      int d=InpDailyHour*60+InpDailyMinute;
      if(m>=d && m<d+InpKeepClosingMinutes)
        {
         reason="daily close time";
         return(true);
        }
     }
   return(false);
  }

void ShowStatus(const datetime now,const bool active,const string reason)
  {
   if(!InpShowStatus)
      return;
   string daily=InpUseDailyClose ? StringFormat("%02d:%02d Mon-Fri",InpDailyHour,InpDailyMinute) : "off";
   string fri=InpUseFridayCutoff ? StringFormat("%02d:%02d",InpFridayHour,InpFridayMinute) : "off";
   Comment(StringFormat("FXPIPS Session Close\nServer time: %s\nDaily close: %s\nFriday cutoff: %s\nStatus: %s\nMatching positions: %d",
                        TimeToString(now,TIME_DATE|TIME_MINUTES),daily,fri,
                        active ? "CLOSING (" + reason + ")" : "waiting",CountFiltered()));
  }

void Check()
  {
   datetime now=TimeTradeServer();
   string reason="";
   bool active=InCloseWindow(now,reason);
   if(active && now-g_lastAttempt>=5)
     {
      g_lastAttempt=now;
      if(InpClosePositions)
         CloseFiltered(reason);
      if(InpDeletePendings)
         DeleteFilteredPendings(reason);
     }
   ShowStatus(now,active,reason);
  }

void OnTick()
  {
   Check();
  }

void OnTimer()
  {
   Check();
  }
//+------------------------------------------------------------------+
