//+------------------------------------------------------------------+
//|                                             FXP_SessionBoxes.mq5 |
//|                                                    FXPIPS ONLINE |
//+------------------------------------------------------------------+
#property copyright "FXPIPS ONLINE"
#property link      "https://fxpips.online/mt5-indicators/forex-session-boxes/"
#property version   "1.00"
#property description "Draws the Asia, London and New York session ranges as boxes."
#property description "Session hours are local exchange time; set your server GMT offset."
/*
   License: MIT. Copyright (c) FXPIPS ONLINE.
   Educational tool - not financial advice. Trading involves substantial risk of loss.
*/
#property indicator_chart_window
#property indicator_buffers 0
#property indicator_plots   0

enum ENUM_FXP_SERVER_DST
  {
   SDST_NONE = 0,   // Fixed offset (no daylight saving)
   SDST_US   = 1,   // +1 hour during US daylight saving
   SDST_EU   = 2    // +1 hour during EU daylight saving
  };

enum ENUM_FXP_ZONE
  {
   ZONE_UTC     = 0,  // UTC
   ZONE_TOKYO   = 1,  // Tokyo (UTC+9)
   ZONE_LONDON  = 2,  // London (UK daylight saving)
   ZONE_NEWYORK = 3,  // New York (US daylight saving)
   ZONE_SERVER  = 4   // Broker server time
  };

input group "Server time"
input int    InpServerGMT   = 2;          // Server GMT offset in winter (hours)
input ENUM_FXP_SERVER_DST InpServerDST = SDST_US; // Server daylight-saving rule
input group "Asia session (Tokyo time)"
input bool   InpAsiaOn      = true;       // Show Asia session
input string InpAsiaStart   = "09:00";    // Start (HH:MM, Tokyo)
input string InpAsiaEnd     = "18:00";    // End (HH:MM, Tokyo)
input color  InpAsiaColor   = C'40,55,90';  // Asia box color
input group "London session (London time)"
input bool   InpLondonOn    = true;       // Show London session
input string InpLondonStart = "08:00";    // Start (HH:MM, London)
input string InpLondonEnd   = "17:00";    // End (HH:MM, London)
input color  InpLondonColor = C'35,80,55';  // London box color
input group "New York session (New York time)"
input bool   InpNYOn        = true;       // Show New York session
input string InpNYStart     = "08:00";    // Start (HH:MM, New York)
input string InpNYEnd       = "17:00";    // End (HH:MM, New York)
input color  InpNYColor     = C'95,45,45';  // New York box color
input group "Display"
input int    InpDaysBack    = 10;         // Days of history to draw
input bool   InpFill        = true;       // Fill boxes
input bool   InpShowLabels  = true;       // Show session name and range (pips)
input color  InpTextColor   = clrSilver;  // Label color

const string PREFIX = "FXP_SB_";
#define NSESS 3

string   g_names[NSESS] = {"Asia", "London", "New York"};
bool     g_on[NSESS];
int      g_start[NSESS], g_end[NSESS];
int      g_zone[NSESS];
color    g_color[NSESS];
datetime g_curId[NSESS];
datetime g_first[NSESS];
double   g_hi[NSESS], g_lo[NSESS];
bool     g_enabled = true;

//+------------------------------------------------------------------+
//| Time-zone helpers                                                |
//+------------------------------------------------------------------+
datetime MakeDate(int y, int mon, int d, int h)
  {
   MqlDateTime s;
   ZeroMemory(s);
   s.year = y;
   s.mon = mon;
   s.day = d;
   s.hour = h;
   return(StructToTime(s));
  }

datetime NthSunday(int y, int mon, int n, int hourUtc)
  {
   MqlDateTime s;
   TimeToStruct(MakeDate(y, mon, 1, 0), s);
   int day = 1 + (7 - s.day_of_week) % 7 + 7 * (n - 1);
   return(MakeDate(y, mon, day, hourUtc));
  }

datetime LastSunday(int y, int mon, int hourUtc)
  {
   int ny = y, nm = mon + 1;
   if(nm > 12)
     {
      nm = 1;
      ny++;
     }
   MqlDateTime s;
   TimeToStruct(MakeDate(ny, nm, 1, 0) - 86400, s);
   return(MakeDate(y, mon, s.day - s.day_of_week, hourUtc));
  }

int YearOf(datetime t)
  {
   MqlDateTime s;
   TimeToStruct(t, s);
   return(s.year);
  }

bool UsDst(datetime utc)
  {
   int y = YearOf(utc);
   return(utc >= NthSunday(y, 3, 2, 7) && utc < NthSunday(y, 11, 1, 6));
  }

bool EuDst(datetime utc)
  {
   int y = YearOf(utc);
   return(utc >= LastSunday(y, 3, 1) && utc < LastSunday(y, 10, 1));
  }

datetime ServerToUtc(datetime t)
  {
   datetime base = t - InpServerGMT * 3600;
   bool dst = (InpServerDST == SDST_US) ? UsDst(base) : (InpServerDST == SDST_EU ? EuDst(base) : false);
   return(base - (dst ? 3600 : 0));
  }

datetime ToZone(datetime serverTime, int zone)
  {
   if(zone == ZONE_SERVER)
      return(serverTime);
   datetime utc = ServerToUtc(serverTime);
   if(zone == ZONE_TOKYO)
      return(utc + 9 * 3600);
   if(zone == ZONE_LONDON)
      return(utc + (EuDst(utc) ? 3600 : 0));
   if(zone == ZONE_NEWYORK)
      return(utc - (UsDst(utc) ? 4 : 5) * 3600);
   return(utc);
  }

int ParseHHMM(string s)
  {
   string parts[];
   if(StringSplit(s, ':', parts) != 2)
      return(-1);
   int h = (int)StringToInteger(parts[0]);
   int m = (int)StringToInteger(parts[1]);
   if(h < 0 || h > 24 || m < 0 || m > 59 || h * 60 + m > 1440)
      return(-1);
   return(h * 60 + m);
  }

// returns the session id (local day of the session start) or 0 when outside
datetime SessionId(datetime serverTime, int s)
  {
   datetime local = ToZone(serverTime, g_zone[s]);
   int m = (int)((local % 86400) / 60);
   datetime day = local - (local % 86400);
   if(g_start[s] < g_end[s])
      return((m >= g_start[s] && m < g_end[s]) ? day : 0);
   if(m >= g_start[s])
      return(day);
   if(m < g_end[s])
      return(day - 86400);
   return(0);
  }

//+------------------------------------------------------------------+
int OnInit()
  {
   g_on[0] = InpAsiaOn;
   g_on[1] = InpLondonOn;
   g_on[2] = InpNYOn;
   g_start[0] = ParseHHMM(InpAsiaStart);
   g_end[0] = ParseHHMM(InpAsiaEnd);
   g_start[1] = ParseHHMM(InpLondonStart);
   g_end[1] = ParseHHMM(InpLondonEnd);
   g_start[2] = ParseHHMM(InpNYStart);
   g_end[2] = ParseHHMM(InpNYEnd);
   g_zone[0] = ZONE_TOKYO;
   g_zone[1] = ZONE_LONDON;
   g_zone[2] = ZONE_NEWYORK;
   g_color[0] = InpAsiaColor;
   g_color[1] = InpLondonColor;
   g_color[2] = InpNYColor;
   for(int s = 0; s < NSESS; s++)
      if(g_start[s] < 0 || g_end[s] < 0 || g_start[s] == g_end[s])
        {
         Print("FXP Session Boxes: invalid time for ", g_names[s], " (use HH:MM)");
         return(INIT_PARAMETERS_INCORRECT);
        }
   g_enabled = (PeriodSeconds(_Period) <= 3600);
   if(!g_enabled)
      Print("FXP Session Boxes: use a timeframe of H1 or lower");
   IndicatorSetString(INDICATOR_SHORTNAME, "FXP Session Boxes");
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   ObjectsDeleteAll(0, PREFIX);
   ChartRedraw();
  }

//+------------------------------------------------------------------+
double PipSize()
  {
   return((_Digits == 3 || _Digits == 5) ? _Point * 10.0 : _Point);
  }

//+------------------------------------------------------------------+
void DrawBox(int s, datetime t2)
  {
   string id = IntegerToString((long)g_curId[s]);
   string name = PREFIX + IntegerToString(s) + "_" + id;
   if(ObjectFind(0, name) < 0)
     {
      ObjectCreate(0, name, OBJ_RECTANGLE, 0, g_first[s], g_hi[s], t2, g_lo[s]);
      ObjectSetInteger(0, name, OBJPROP_COLOR, g_color[s]);
      ObjectSetInteger(0, name, OBJPROP_FILL, InpFill);
      ObjectSetInteger(0, name, OBJPROP_BACK, true);
      ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
      ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
     }
   ObjectMove(0, name, 0, g_first[s], g_hi[s]);
   ObjectMove(0, name, 1, t2, g_lo[s]);
   double pips = (g_hi[s] - g_lo[s]) / PipSize();
   ObjectSetString(0, name, OBJPROP_TOOLTIP, StringFormat("%s range %.1f pips", g_names[s], pips));
   if(!InpShowLabels)
      return;
   string lbl = name + "_T";
   if(ObjectFind(0, lbl) < 0)
     {
      ObjectCreate(0, lbl, OBJ_TEXT, 0, g_first[s], g_hi[s]);
      ObjectSetInteger(0, lbl, OBJPROP_ANCHOR, ANCHOR_LEFT_LOWER);
      ObjectSetInteger(0, lbl, OBJPROP_FONTSIZE, 8);
      ObjectSetInteger(0, lbl, OBJPROP_COLOR, InpTextColor);
      ObjectSetInteger(0, lbl, OBJPROP_SELECTABLE, false);
      ObjectSetInteger(0, lbl, OBJPROP_HIDDEN, true);
     }
   ObjectMove(0, lbl, 0, g_first[s], g_hi[s]);
   ObjectSetString(0, lbl, OBJPROP_TEXT, StringFormat("%s %.1f", g_names[s], pips));
  }

//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
   if(!g_enabled || rates_total < 2)
      return(rates_total);
   ArraySetAsSeries(time, false);
   ArraySetAsSeries(high, false);
   ArraySetAsSeries(low, false);

   int start;
   if(prev_calculated == 0)
     {
      ObjectsDeleteAll(0, PREFIX);
      ArrayInitialize(g_curId, 0);
      datetime from = time[rates_total - 1] - (datetime)InpDaysBack * 86400;
      start = rates_total - 1;
      while(start > 0 && time[start - 1] >= from)
         start--;
     }
   else
      start = prev_calculated - 1;

   int per = PeriodSeconds(_Period);
   for(int i = start; i < rates_total; i++)
     {
      for(int s = 0; s < NSESS; s++)
        {
         if(!g_on[s])
            continue;
         datetime id = SessionId(time[i], s);
         if(id == 0)
            continue;
         if(id != g_curId[s])
           {
            g_curId[s] = id;
            g_first[s] = time[i];
            g_hi[s] = high[i];
            g_lo[s] = low[i];
           }
         else
           {
            g_hi[s] = MathMax(g_hi[s], high[i]);
            g_lo[s] = MathMin(g_lo[s], low[i]);
           }
         DrawBox(s, time[i] + per);
        }
     }
   return(rates_total);
  }
//+------------------------------------------------------------------+
