dinexspace-free-indicators/Indicators/DinexSessionBoxes.mq5
406 lines
15 KiB
MQL5
//+------------------------------------------------------------------+
//| DinexSpace | Session Boxes |
//| Draws the Asia (Tokyo), Europe (London) and US (New York) |
//| sessions as boxes spanning each session's high and low. |
//| Session hours are entered in each market's local time; London |
//| and New York daylight saving is applied automatically. |
//+------------------------------------------------------------------+
#property copyright "(c) 2026 DinexSpace"
#property version "1.00"
#property description "Asia, Europe and US trading sessions as boxes with their high-low range."
#property description "Hours are in each market's local time; daylight saving is handled automatically."
#property strict
#property indicator_chart_window
#property indicator_buffers 0
#property indicator_plots 0
//--- server time handling
enum ENUM_SERVER_TZ
{
SERVER_AUTO_US_DST = 0, // Auto, broker shifts with US DST (most brokers)
SERVER_AUTO_FIXED = 1, // Auto, fixed offset all year
SERVER_MANUAL = 2 // Manual offset (below)
};
enum ENUM_RANGE_UNIT
{
UNIT_PRICE = 0, // Price
UNIT_POINTS = 1, // Points
UNIT_PIPS = 2 // Pips (forex)
};
input group "Sessions (hours in each market's local time)"
input bool InpShowAsia = true; // Show Asia (Tokyo, UTC+9)
input int InpAsiaStart = 900; // Asia start (HHMM)
input int InpAsiaEnd = 1800; // Asia end (HHMM)
input color InpAsiaColor = C'60,50,120'; // Asia colour
input bool InpShowLondon = true; // Show Europe (London, UK daylight saving)
input int InpLonStart = 800; // Europe start (HHMM)
input int InpLonEnd = 1700; // Europe end (HHMM)
input color InpLonColor = C'110,80,0'; // Europe colour
input bool InpShowNY = true; // Show US (New York, US daylight saving)
input int InpNYStart = 800; // US start (HHMM)
input int InpNYEnd = 1700; // US end (HHMM)
input color InpNYColor = C'110,40,70'; // US colour
input group "Server time"
input ENUM_SERVER_TZ InpServerTz = SERVER_AUTO_US_DST; // Server time zone
input double InpServerOffset = 2.0; // Manual server offset from UTC (hours)
input group "Display"
input bool InpFill = true; // Fill boxes (drawn behind candles)
input bool InpLabels = true; // Label with name and range
input ENUM_RANGE_UNIT InpUnit = UNIT_PRICE; // Range unit
input bool InpMidLine = false; // Mid-line (50% of range)
input int InpKeep = 20; // Sessions to keep on chart (each)
input int InpMaxBars = 5000; // Bars to scan
input bool InpPanel = true; // Status panel
input ENUM_BASE_CORNER InpCorner = CORNER_RIGHT_UPPER; // Panel corner
#define NS 3
const string PFX = "DNXSB_";
string g_name[NS] = {"Asia", "London", "New York"};
bool g_show[NS];
int g_start[NS], g_end[NS]; // minutes of day, local
color g_col[NS];
datetime g_key[NS]; // start time (UTC) of the session being tracked
datetime g_t0[NS]; // server time of first bar of that session
double g_hi[NS], g_lo[NS];
bool g_open[NS]; // session contains the latest bar
struct SHist { datetime k[]; }; // keys of drawn sessions (oldest first)
SHist g_hist[NS];
//+------------------------------------------------------------------+
//| Calendar helpers |
//+------------------------------------------------------------------+
datetime MakeDate(int y, int m, int d)
{
MqlDateTime s;
ZeroMemory(s);
s.year = y; s.mon = m; s.day = d;
return StructToTime(s);
}
// Day of month of the n-th Sunday (n >= 1) or the last Sunday (n = 0) of a month.
int SundayOf(int y, int m, int n)
{
if(n > 0)
{
MqlDateTime s;
TimeToStruct(MakeDate(y, m, 1), s);
int first = 1 + (7 - s.day_of_week) % 7;
return first + 7 * (n - 1);
}
int nm = (m == 12) ? 1 : m + 1, ny = (m == 12) ? y + 1 : y;
MqlDateTime e;
TimeToStruct(MakeDate(ny, nm, 1) - 86400, e);
return e.day - e.day_of_week;
}
// US daylight saving: second Sunday of March 07:00 UTC to first Sunday of November 06:00 UTC.
bool IsUsDst(datetime utc)
{
MqlDateTime s;
TimeToStruct(utc, s);
datetime a = MakeDate(s.year, 3, SundayOf(s.year, 3, 2)) + 7 * 3600;
datetime b = MakeDate(s.year, 11, SundayOf(s.year, 11, 1)) + 6 * 3600;
return utc >= a && utc < b;
}
// EU daylight saving: last Sunday of March 01:00 UTC to last Sunday of October 01:00 UTC.
bool IsEuDst(datetime utc)
{
MqlDateTime s;
TimeToStruct(utc, s);
datetime a = MakeDate(s.year, 3, SundayOf(s.year, 3, 0)) + 3600;
datetime b = MakeDate(s.year, 10, SundayOf(s.year, 10, 0)) + 3600;
return utc >= a && utc < b;
}
//+------------------------------------------------------------------+
//| Time zones |
//+------------------------------------------------------------------+
int g_baseOffset = 0; // server offset from UTC in seconds (standard time for SERVER_AUTO_US_DST)
void DetectServerOffset()
{
if(InpServerTz == SERVER_MANUAL)
{
g_baseOffset = (int)MathRound(InpServerOffset * 3600.0);
return;
}
datetime srv = TimeTradeServer(), gmt = TimeGMT();
int off = (int)MathRound((double)(srv - gmt) / 1800.0) * 1800;
if(InpServerTz == SERVER_AUTO_US_DST && IsUsDst(gmt))
off -= 3600;
g_baseOffset = off;
}
datetime ServerToUtc(datetime t)
{
datetime u = t - g_baseOffset;
if(InpServerTz == SERVER_AUTO_US_DST && IsUsDst(u))
u -= 3600;
return u;
}
// Offset of session market s from UTC at time utc, in seconds.
int MarketOffset(int s, datetime utc)
{
if(s == 0)
return 9 * 3600; // Tokyo, no daylight saving
if(s == 1)
return IsEuDst(utc) ? 3600 : 0; // London
return IsUsDst(utc) ? -4 * 3600 : -5 * 3600; // New York
}
int HhmmToMin(int v) { return (v / 100) * 60 + v % 100; }
// If the bar at UTC time `utc` is inside session s, returns true and the session's start time (UTC).
bool InSession(int s, datetime utc, datetime &key)
{
int off = MarketOffset(s, utc);
datetime loc = utc + off;
MqlDateTime d;
TimeToStruct(loc, d);
int mins = d.hour * 60 + d.min;
int st = g_start[s], en = g_end[s];
datetime dayStart = loc - (d.hour * 3600 + d.min * 60 + d.sec);
bool inS;
datetime startLoc;
if(st < en)
{
inS = mins >= st && mins < en;
startLoc = dayStart + st * 60;
}
else
{
inS = mins >= st || mins < en;
startLoc = (mins >= st) ? dayStart + st * 60 : dayStart - 86400 + st * 60;
}
if(!inS)
return false;
MqlDateTime sd;
TimeToStruct(startLoc, sd);
if(sd.day_of_week == 0 || sd.day_of_week == 6)
return false; // no sessions starting on weekends
key = startLoc - off;
return true;
}
//+------------------------------------------------------------------+
//| Drawing |
//+------------------------------------------------------------------+
string FmtRange(double r)
{
if(InpUnit == UNIT_POINTS)
return IntegerToString((long)MathRound(r / _Point)) + " pts";
if(InpUnit == UNIT_PIPS)
{
double pip = (_Digits == 3 || _Digits == 5) ? _Point * 10 : _Point;
return DoubleToString(r / pip, 1) + " pips";
}
return DoubleToString(r, _Digits);
}
string ObjName(int s, datetime key, string part)
{
return PFX + IntegerToString(s) + "_" + IntegerToString((long)key) + "_" + part;
}
void DeleteSession(int s, datetime key)
{
ObjectDelete(0, ObjName(s, key, "box"));
ObjectDelete(0, ObjName(s, key, "lbl"));
ObjectDelete(0, ObjName(s, key, "mid"));
}
void DrawSession(int s, datetime key, datetime t0, datetime t1, double hi, double lo)
{
string bx = ObjName(s, key, "box");
if(ObjectFind(0, bx) < 0)
{
ObjectCreate(0, bx, OBJ_RECTANGLE, 0, t0, hi, t1, lo);
ObjectSetInteger(0, bx, OBJPROP_COLOR, g_col[s]);
ObjectSetInteger(0, bx, OBJPROP_FILL, InpFill);
ObjectSetInteger(0, bx, OBJPROP_BACK, true);
ObjectSetInteger(0, bx, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, bx, OBJPROP_HIDDEN, true);
}
ObjectMove(0, bx, 0, t0, hi);
ObjectMove(0, bx, 1, t1, lo);
if(InpLabels)
{
string lb = ObjName(s, key, "lbl");
if(ObjectFind(0, lb) < 0)
{
ObjectCreate(0, lb, OBJ_TEXT, 0, t0, hi);
ObjectSetInteger(0, lb, OBJPROP_ANCHOR, ANCHOR_LEFT_LOWER);
ObjectSetInteger(0, lb, OBJPROP_FONTSIZE, 8);
ObjectSetInteger(0, lb, OBJPROP_COLOR, clrSilver);
ObjectSetInteger(0, lb, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, lb, OBJPROP_HIDDEN, true);
}
ObjectMove(0, lb, 0, t0, hi);
ObjectSetString(0, lb, OBJPROP_TEXT, g_name[s] + " " + FmtRange(hi - lo));
}
if(InpMidLine)
{
string md = ObjName(s, key, "mid");
double mid = (hi + lo) / 2.0;
if(ObjectFind(0, md) < 0)
{
ObjectCreate(0, md, OBJ_TREND, 0, t0, mid, t1, mid);
ObjectSetInteger(0, md, OBJPROP_COLOR, g_col[s]);
ObjectSetInteger(0, md, OBJPROP_STYLE, STYLE_DOT);
ObjectSetInteger(0, md, OBJPROP_RAY_RIGHT, false);
ObjectSetInteger(0, md, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, md, OBJPROP_HIDDEN, true);
}
ObjectMove(0, md, 0, t0, mid);
ObjectMove(0, md, 1, t1, mid);
}
}
void PanelLine(int row, string text, color col)
{
string nm = PFX + "panel_" + IntegerToString(row);
if(ObjectFind(0, nm) < 0)
{
ObjectCreate(0, nm, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, nm, OBJPROP_CORNER, InpCorner);
bool right = (InpCorner == CORNER_RIGHT_UPPER || InpCorner == CORNER_RIGHT_LOWER);
bool lower = (InpCorner == CORNER_LEFT_LOWER || InpCorner == CORNER_RIGHT_LOWER);
ObjectSetInteger(0, nm, OBJPROP_ANCHOR, right ? (lower ? ANCHOR_RIGHT_LOWER : ANCHOR_RIGHT_UPPER)
: (lower ? ANCHOR_LEFT_LOWER : ANCHOR_LEFT_UPPER));
ObjectSetInteger(0, nm, OBJPROP_XDISTANCE, 10);
ObjectSetInteger(0, nm, OBJPROP_YDISTANCE, 20 + (lower ? 4 - row : row) * 16);
ObjectSetInteger(0, nm, OBJPROP_FONTSIZE, 9);
ObjectSetString(0, nm, OBJPROP_FONT, "Consolas");
ObjectSetInteger(0, nm, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, nm, OBJPROP_HIDDEN, true);
}
ObjectSetString(0, nm, OBJPROP_TEXT, text);
ObjectSetInteger(0, nm, OBJPROP_COLOR, col);
}
void UpdatePanel()
{
if(!InpPanel)
return;
if(PeriodSeconds(_Period) >= 86400)
{
PanelLine(0, "Sessions need an intraday chart", clrOrange);
PanelLine(4, "DinexSpace", clrSpringGreen);
return;
}
PanelLine(0, "Session Status Range", clrGray);
for(int s = 0; s < NS; s++)
{
string rng = (g_key[s] > 0) ? FmtRange(g_hi[s] - g_lo[s]) : "-";
string st = !g_show[s] ? "off" : (g_open[s] ? "open" : "closed");
PanelLine(1 + s, StringFormat("%-12s %-8s %s", g_name[s], st, rng), g_open[s] ? clrSpringGreen : clrSilver);
}
PanelLine(4, "DinexSpace", clrSpringGreen);
}
//+------------------------------------------------------------------+
//| Indicator events |
//+------------------------------------------------------------------+
void ResetState()
{
for(int s = 0; s < NS; s++)
{
g_key[s] = 0; g_t0[s] = 0; g_hi[s] = 0; g_lo[s] = 0; g_open[s] = false;
ArrayResize(g_hist[s].k, 0);
}
}
int OnInit()
{
g_show[0] = InpShowAsia; g_start[0] = HhmmToMin(InpAsiaStart); g_end[0] = HhmmToMin(InpAsiaEnd); g_col[0] = InpAsiaColor;
g_show[1] = InpShowLondon; g_start[1] = HhmmToMin(InpLonStart); g_end[1] = HhmmToMin(InpLonEnd); g_col[1] = InpLonColor;
g_show[2] = InpShowNY; g_start[2] = HhmmToMin(InpNYStart); g_end[2] = HhmmToMin(InpNYEnd); g_col[2] = InpNYColor;
for(int s = 0; s < NS; s++)
if(g_start[s] < 0 || g_start[s] >= 1440 || g_end[s] < 0 || g_end[s] >= 1440 || g_start[s] == g_end[s])
{
Print("DinexSpace Session Boxes: invalid hours for ", g_name[s], " (use HHMM, start different from end)");
return INIT_PARAMETERS_INCORRECT;
}
if(InpKeep < 1 || InpMaxBars < 10)
return INIT_PARAMETERS_INCORRECT;
DetectServerOffset();
ResetState();
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
ObjectsDeleteAll(0, PFX);
}
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(rates_total < 2)
return 0;
ArraySetAsSeries(time, false);
ArraySetAsSeries(high, false);
ArraySetAsSeries(low, false);
if(PeriodSeconds(_Period) >= 86400)
{
UpdatePanel();
return rates_total;
}
int from;
if(prev_calculated == 0)
{
ObjectsDeleteAll(0, PFX);
DetectServerOffset();
ResetState();
from = MathMax(0, rates_total - InpMaxBars);
}
else
from = prev_calculated - 1;
for(int i = from; i < rates_total; i++)
{
datetime utc = ServerToUtc(time[i]);
for(int s = 0; s < NS; s++)
{
if(!g_show[s])
continue;
datetime key;
bool inS = InSession(s, utc, key);
if(i == rates_total - 1)
g_open[s] = inS;
if(!inS)
continue;
if(key != g_key[s])
{
// a new session starts on this bar
g_key[s] = key; g_t0[s] = time[i]; g_hi[s] = high[i]; g_lo[s] = low[i];
int n = ArraySize(g_hist[s].k);
ArrayResize(g_hist[s].k, n + 1);
g_hist[s].k[n] = key;
if(n + 1 > InpKeep)
{
DeleteSession(s, g_hist[s].k[0]);
ArrayRemove(g_hist[s].k, 0, 1);
}
}
else
{
g_hi[s] = MathMax(g_hi[s], high[i]);
g_lo[s] = MathMin(g_lo[s], low[i]);
}
DrawSession(s, g_key[s], g_t0[s], time[i], g_hi[s], g_lo[s]);
}
}
UpdatePanel();
return rates_total;
}
//+------------------------------------------------------------------+