894 lines
30 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| ICTMacros.mq5 |
| |||
//| Copyright 2026, MT5 Coder Project |
| |||
//+------------------------------------------------------------------+
| |||
// WHAT IT DOES
| |||
// Marks ICT macro windows as chart objects (rectangle, plus optional
| |||
// 50% midline, start/end vertical lines and a label) for the Asia,
| |||
// London and New York sessions. Every session has its own ON/OFF input.
| |||
//
| |||
// Window times are entered in NEW YORK local time as
| |||
// "HH:MM-HH:MM;HH:MM-HH:MM". Defaults:
| |||
// London : 02:33-03:00;04:03-04:30
| |||
// New York: 08:50-09:10;09:50-10:10;10:50-11:10;11:50-12:10;13:10-13:40;15:15-15:45
| |||
// Asia : empty by default - paste Asia windows into InpAsiaMacros and
| |||
// the Asia toggle starts working immediately.
| |||
// A window whose end time is not later than its start time is treated as
| |||
// crossing midnight (e.g. 23:30-00:15).
| |||
//
| |||
// TIME BASE
| |||
// Chart bars are in broker server time. Window times are converted
| |||
// NY local -> UTC (US DST rules implemented here) -> server time.
| |||
// The server/UTC offset is auto-detected as TimeTradeServer()-TimeGMT()
| |||
// rounded to 5 minutes, or taken from InpManualServerGmtOffset.
| |||
// TimeGMT() depends on the accuracy of the local PC clock; switch to
| |||
// ICT_SRV_MANUAL if that clock is not reliable.
| |||
//
| |||
// OBJECTS
| |||
// Prefix "ICTMacros_". Only objects created by this program are removed.
| |||
// Windows on closed bars never move. The window that is currently open is
| |||
// refreshed while it is open (InpLiveBox). Windows whose bars do not exist
| |||
// yet are not drawn, so a later window of the day appears once its first
| |||
// bar has formed.
| |||
//
| |||
// LIMITATIONS
| |||
// * Chart objects only - no indicator buffers, nothing for iCustom.
| |||
// * MT5 chart objects have no alpha transparency: a filled box is opaque
| |||
// (it is drawn behind the bars). Outline only is the default.
| |||
// * Best on M1-M15. On coarse timeframes a window can cover 0 or 1 bar.
| |||
// * Object count grows with InpHistoryDays - watch InpMaxObjects.
| |||
// * Visual tool only: it draws no signals and does not trade.
| |||
//+------------------------------------------------------------------+
| |||
#property copyright "Copyright 2026, MT5 Coder Project"
| |||
#property link "https://www.mql5.com"
| |||
#property version "1.00"
| |||
#property description "ICT macro windows (Asia / London / New York) drawn in New York local time."
| |||
| |||
#property indicator_chart_window
| |||
#property indicator_buffers 0
| |||
#property indicator_plots 0
| |||
| |||
//--- object namespace owned by this program
| |||
#define ICT_PREFIX "ICTMacros_"
| |||
#define ICT_SEC_PER_DAY 86400
| |||
#define ICT_MIN_PER_DAY 1440
| |||
| |||
//--- enums
| |||
enum ENUM_ICT_TIMEBASE { ICT_TB_NEWYORK = 0, ICT_TB_SERVER = 1, ICT_TB_GMT = 2 };
| |||
enum ENUM_ICT_SRVOFF { ICT_SRV_AUTO = 0, ICT_SRV_MANUAL = 1 };
| |||
enum ENUM_ICT_SESSION { ICT_ASIA = 0, ICT_LONDON = 1, ICT_NEWYORK = 2 };
| |||
| |||
//--- inputs
| |||
input group "Sessions"
| |||
input bool InpShowAsia = true; // Draw Asia macros
| |||
input bool InpShowLondon = true; // Draw London macros
| |||
input bool InpShowNewYork = true; // Draw New York macros
| |||
| |||
input group "Macro windows (New York local time, format HH:MM-HH:MM;...)"
| |||
input string InpAsiaMacros = ""; // Asia windows (empty = none)
| |||
input string InpLondonMacros = "02:33-03:00;04:03-04:30";
| |||
input string InpNYMacros = "08:50-09:10;09:50-10:10;10:50-11:10;11:50-12:10;13:10-13:40;15:15-15:45";
| |||
| |||
input group "Time base"
| |||
input ENUM_ICT_TIMEBASE InpTimeBase = ICT_TB_NEWYORK; // Reference zone of the windows above
| |||
input ENUM_ICT_SRVOFF InpServerOffsetMode = ICT_SRV_AUTO; // Broker server offset detection
| |||
input double InpManualServerGmtOffset = 0.0; // Server GMT offset in hours (manual mode)
| |||
| |||
input group "Display"
| |||
input bool InpDrawBox = true; // Draw the macro box
| |||
input bool InpFillBox = false; // Fill the box (opaque, drawn behind the bars)
| |||
input bool InpMidline = false; // Draw the 50% midline of the window
| |||
input bool InpVerticalLines = true; // Draw vertical lines at window start/end
| |||
input bool InpShowLabels = true; // Draw the window label
| |||
input int InpLabelFontSize = 8; // Label font size
| |||
input color InpAsiaColor = clrKhaki;
| |||
input color InpLondonColor = clrSteelBlue;
| |||
input color InpNewYorkColor = clrTomato;
| |||
input bool InpLiveBox = true; // Refresh the window that is currently open
| |||
| |||
input group "History / limits"
| |||
input int InpHistoryDays = 20; // Past days to draw (0 = current day only)
| |||
input int InpMaxObjects = 2000; // Safety cap of objects per rebuild
| |||
input bool InpCleanStaleObjects = true; // Remove leftover ICTMacros_ objects of former runs
| |||
| |||
input group "Diagnostics"
| |||
input bool InpVerboseLog = true; // Print time base / offset / object diagnostics
| |||
| |||
//--- one macro window (times are minutes from midnight in the reference zone)
| |||
struct MacroWindow
| |||
{
| |||
int session; // ENUM_ICT_SESSION
| |||
int index; // 1-based index inside the session
| |||
int startMin; // start, minutes from midnight
| |||
int endMin; // end, minutes from midnight (may exceed 1440 when crossing midnight)
| |||
string label; // display label, e.g. "London 02:33-03:00"
| |||
};
| |||
| |||
//--- state
| |||
MacroWindow g_win[]; // parsed windows of all enabled session lists
| |||
int g_winCount = 0;
| |||
string g_names[]; // objects of the current rebuild
| |||
int g_nameCount = 0;
| |||
string g_prevNames[]; // objects of the previous rebuild (stale-object diff)
| |||
int g_prevCount = 0;
| |||
datetime g_lastBarTime = 0;
| |||
int g_srvOffsetSec = 0; // broker server UTC offset in seconds
| |||
double g_liveHi = 0.0;
| |||
double g_liveLo = 0.0;
| |||
bool g_loggedSummary = false;
| |||
bool g_capWarned = false;
| |||
bool g_orphanSwept = false;
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Small text helpers |
| |||
//+------------------------------------------------------------------+
| |||
string Trim(const string src)
| |||
{
| |||
string s = src;
| |||
StringTrimLeft(s);
| |||
StringTrimRight(s);
| |||
return s;
| |||
}
| |||
| |||
bool AllDigits(const string s)
| |||
{
| |||
const int n = StringLen(s);
| |||
if(n == 0)
| |||
return false;
| |||
for(int i = 0; i < n; i++)
| |||
{
| |||
const ushort c = StringGetCharacter(s, i);
| |||
if(c < 48 || c > 57) // '0' .. '9'
| |||
return false;
| |||
}
| |||
return true;
| |||
}
| |||
| |||
//| Parse "HH:MM" into minutes from midnight. |
| |||
bool ParseHHMM(const string src, int &minutes)
| |||
{
| |||
const string t = Trim(src);
| |||
string hm[];
| |||
if(StringSplit(t, ':', hm) != 2)
| |||
return false;
| |||
const string hs = Trim(hm[0]);
| |||
const string ms = Trim(hm[1]);
| |||
if(!AllDigits(hs) || !AllDigits(ms))
| |||
return false;
| |||
const int h = (int)StringToInteger(hs);
| |||
const int m = (int)StringToInteger(ms);
| |||
if(h < 0 || h > 23 || m < 0 || m > 59)
| |||
return false;
| |||
minutes = h * 60 + m;
| |||
return true;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Session helpers |
| |||
//+------------------------------------------------------------------+
| |||
string SessionTag(const int session)
| |||
{
| |||
switch(session)
| |||
{
| |||
case ICT_ASIA: return "Asia";
| |||
case ICT_LONDON: return "London";
| |||
case ICT_NEWYORK: return "NY";
| |||
}
| |||
return "Sess";
| |||
}
| |||
| |||
string SessionLabel(const int session)
| |||
{
| |||
switch(session)
| |||
{
| |||
case ICT_ASIA: return "Asia";
| |||
case ICT_LONDON: return "London";
| |||
case ICT_NEWYORK: return "New York";
| |||
}
| |||
return "Session";
| |||
}
| |||
| |||
color SessionColor(const int session)
| |||
{
| |||
switch(session)
| |||
{
| |||
case ICT_ASIA: return InpAsiaColor;
| |||
case ICT_LONDON: return InpLondonColor;
| |||
case ICT_NEWYORK: return InpNewYorkColor;
| |||
}
| |||
return clrGray;
| |||
}
| |||
| |||
bool SessionEnabled(const int session)
| |||
{
| |||
switch(session)
| |||
{
| |||
case ICT_ASIA: return InpShowAsia;
| |||
case ICT_LONDON: return InpShowLondon;
| |||
case ICT_NEWYORK: return InpShowNewYork;
| |||
}
| |||
return false;
| |||
}
| |||
| |||
string TimeBaseName(const ENUM_ICT_TIMEBASE tb)
| |||
{
| |||
switch(tb)
| |||
{
| |||
case ICT_TB_NEWYORK: return "NewYork";
| |||
case ICT_TB_SERVER: return "Server";
| |||
case ICT_TB_GMT: return "GMT";
| |||
}
| |||
return "Unknown";
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Parse one session window list, appending to dst. |
| |||
//| Returns the number of windows added. Invalid tokens are skipped |
| |||
//| with a diagnostic instead of aborting the whole list. |
| |||
//+------------------------------------------------------------------+
| |||
int ParseWindowList(const string src, const int session, MacroWindow &dst[])
| |||
{
| |||
if(StringLen(Trim(src)) == 0)
| |||
return 0;
| |||
| |||
int added = 0;
| |||
string parts[];
| |||
const int count = StringSplit(src, ';', parts);
| |||
for(int i = 0; i < count; i++)
| |||
{
| |||
const string token = Trim(parts[i]);
| |||
if(token == "")
| |||
continue;
| |||
| |||
string range[];
| |||
if(StringSplit(token, '-', range) != 2)
| |||
{
| |||
PrintFormat("ICTMacros: skipped window '%s' - expected HH:MM-HH:MM", token);
| |||
continue;
| |||
}
| |||
| |||
int startMin = 0, endMin = 0;
| |||
if(!ParseHHMM(range[0], startMin) || !ParseHHMM(range[1], endMin))
| |||
{
| |||
PrintFormat("ICTMacros: skipped window '%s' - invalid time", token);
| |||
continue;
| |||
}
| |||
if(endMin == startMin)
| |||
{
| |||
PrintFormat("ICTMacros: skipped window '%s' - zero length", token);
| |||
continue;
| |||
}
| |||
| |||
const int n = ArraySize(dst);
| |||
if(ArrayResize(dst, n + 1) != n + 1)
| |||
{
| |||
Print("ICTMacros: out of memory while parsing macro windows");
| |||
break;
| |||
}
| |||
dst[n].session = session;
| |||
dst[n].index = added + 1;
| |||
dst[n].startMin = startMin;
| |||
dst[n].endMin = (endMin < startMin) ? endMin + ICT_MIN_PER_DAY : endMin;
| |||
dst[n].label = StringFormat("%s %02d:%02d-%02d:%02d",
| |||
SessionLabel(session),
| |||
startMin / 60, startMin % 60,
| |||
endMin / 60, endMin % 60);
| |||
added++;
| |||
}
| |||
return added;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| America/New_York DST rules |
| |||
//| DST starts 02:00 local (07:00 UTC) on the 2nd Sunday of March, |
| |||
//| DST ends 02:00 local (06:00 UTC) on the 1st Sunday of November|
| |||
//+------------------------------------------------------------------+
| |||
datetime NthSundayUtc(const int year, const int month, const int nth, const int hourUtc)
| |||
{
| |||
MqlDateTime d;
| |||
d.year = year; d.mon = month; d.day = 1; d.hour = hourUtc;
| |||
d.min = 0; d.sec = 0; d.day_of_week = 0; d.day_of_year = 0;
| |||
const datetime first = StructToTime(d);
| |||
| |||
MqlDateTime f;
| |||
TimeToStruct(first, f);
| |||
const int dow = f.day_of_week; // 0 = Sunday
| |||
const int offsetDays = ((7 - dow) % 7) + (nth - 1) * 7;
| |||
return (datetime)((long)first + (long)offsetDays * ICT_SEC_PER_DAY);
| |||
}
| |||
| |||
bool IsUsDst(const datetime utc)
| |||
{
| |||
MqlDateTime d;
| |||
TimeToStruct(utc, d);
| |||
const datetime dstStart = NthSundayUtc(d.year, 3, 2, 7);
| |||
const datetime dstEnd = NthSundayUtc(d.year, 11, 1, 6);
| |||
return (utc >= dstStart && utc < dstEnd);
| |||
}
| |||
| |||
//| NY local = UTC + offset (EDT -4h, EST -5h) |
| |||
int NyUtcOffsetSec(const datetime utc)
| |||
{
| |||
return IsUsDst(utc) ? -4 * 3600 : -5 * 3600;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Time conversion helpers |
| |||
//+------------------------------------------------------------------+
| |||
datetime DayStart(const datetime t)
| |||
{
| |||
return (datetime)((long)t - (long)((long)t % ICT_SEC_PER_DAY));
| |||
}
| |||
| |||
datetime MakeNaiveDay(const int year, const int month, const int day)
| |||
{
| |||
MqlDateTime d;
| |||
d.year = year; d.mon = month; d.day = day;
| |||
d.hour = 0; d.min = 0; d.sec = 0; d.day_of_week = 0; d.day_of_year = 0;
| |||
return StructToTime(d);
| |||
}
| |||
| |||
//| Convert a window boundary (minutes from midnight of refDay in the |
| |||
//| reference zone) into broker server time. |
| |||
datetime WindowTimeToServer(const datetime refDay, int minutesOfDay)
| |||
{
| |||
long extraDays = 0;
| |||
while(minutesOfDay >= ICT_MIN_PER_DAY)
| |||
{
| |||
minutesOfDay -= ICT_MIN_PER_DAY;
| |||
extraDays++;
| |||
}
| |||
const datetime naive = (datetime)((long)refDay + extraDays * ICT_SEC_PER_DAY + (long)minutesOfDay * 60);
| |||
| |||
if(InpTimeBase == ICT_TB_SERVER)
| |||
return naive; // the list is already in server time
| |||
| |||
datetime utc = naive; // for GMT the naive stamp is UTC
| |||
if(InpTimeBase == ICT_TB_NEWYORK)
| |||
{
| |||
const int off1 = NyUtcOffsetSec(naive); // first guess from the naive stamp
| |||
utc = (datetime)((long)naive - (long)off1);
| |||
const int off2 = NyUtcOffsetSec(utc); // second pass, correct across the DST switch
| |||
if(off2 != off1)
| |||
utc = (datetime)((long)naive - (long)off2);
| |||
}
| |||
return (datetime)((long)utc + (long)g_srvOffsetSec);
| |||
}
| |||
| |||
//| "Now" expressed in the wall clock of the configured time base. |
| |||
datetime ReferenceNow()
| |||
{
| |||
const datetime srvNow = TimeTradeServer();
| |||
if(InpTimeBase == ICT_TB_SERVER)
| |||
return srvNow;
| |||
| |||
const datetime utcNow = (datetime)((long)srvNow - (long)g_srvOffsetSec);
| |||
if(InpTimeBase == ICT_TB_GMT)
| |||
return utcNow;
| |||
| |||
return (datetime)((long)utcNow + (long)NyUtcOffsetSec(utcNow));
| |||
}
| |||
| |||
//| Broker server UTC offset in seconds, snapped to 5 minutes. |
| |||
int DetectServerOffsetSec()
| |||
{
| |||
if(InpTimeBase == ICT_TB_SERVER)
| |||
return 0;
| |||
if(InpServerOffsetMode == ICT_SRV_MANUAL)
| |||
return (int)MathRound(InpManualServerGmtOffset * 3600.0 / 300.0) * 300;
| |||
| |||
const double diff = (double)((long)TimeTradeServer() - (long)TimeGMT());
| |||
return (int)MathRound(diff / 300.0) * 300;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| First index whose time is >= value (arrays arrive oldest-first). |
| |||
//+------------------------------------------------------------------+
| |||
int LowerBound(const datetime &time[], const int total, const datetime value)
| |||
{
| |||
int lo = 0, hi = total;
| |||
while(lo < hi)
| |||
{
| |||
const int mid = lo + (hi - lo) / 2;
| |||
if(time[mid] < value)
| |||
lo = mid + 1;
| |||
else
| |||
hi = mid;
| |||
}
| |||
return lo;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Chart object helpers (own namespace only) |
| |||
//+------------------------------------------------------------------+
| |||
void SetCommonProps(const string name, const color clr, const int width, const bool back)
| |||
{
| |||
ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
| |||
ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_SOLID);
| |||
ObjectSetInteger(0, name, OBJPROP_WIDTH, width);
| |||
ObjectSetInteger(0, name, OBJPROP_BACK, back);
| |||
ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
| |||
ObjectSetInteger(0, name, OBJPROP_SELECTED, false);
| |||
ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
| |||
}
| |||
| |||
bool EnsureRect(const string name, const datetime t1, const double p1,
| |||
const datetime t2, const double p2, const color clr)
| |||
{
| |||
if(ObjectFind(0, name) < 0)
| |||
{
| |||
if(!ObjectCreate(0, name, OBJ_RECTANGLE, 0, t1, p1, t2, p2))
| |||
{
| |||
PrintFormat("ICTMacros: ObjectCreate failed for %s (error %d)", name, GetLastError());
| |||
return false;
| |||
}
| |||
}
| |||
else
| |||
{
| |||
ObjectMove(0, name, 0, t1, p1);
| |||
ObjectMove(0, name, 1, t2, p2);
| |||
}
| |||
SetCommonProps(name, clr, 1, true);
| |||
ObjectSetInteger(0, name, OBJPROP_FILL, InpFillBox);
| |||
return true;
| |||
}
| |||
| |||
bool EnsureTrend(const string name, const datetime t1, const double p1,
| |||
const datetime t2, const double p2, const color clr)
| |||
{
| |||
if(ObjectFind(0, name) < 0)
| |||
{
| |||
if(!ObjectCreate(0, name, OBJ_TREND, 0, t1, p1, t2, p2))
| |||
{
| |||
PrintFormat("ICTMacros: ObjectCreate failed for %s (error %d)", name, GetLastError());
| |||
return false;
| |||
}
| |||
}
| |||
else
| |||
{
| |||
ObjectMove(0, name, 0, t1, p1);
| |||
ObjectMove(0, name, 1, t2, p2);
| |||
}
| |||
SetCommonProps(name, clr, 1, true);
| |||
ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_DOT);
| |||
ObjectSetInteger(0, name, OBJPROP_RAY_LEFT, false);
| |||
ObjectSetInteger(0, name, OBJPROP_RAY_RIGHT, false);
| |||
return true;
| |||
}
| |||
| |||
bool EnsureVLine(const string name, const datetime t, const color clr)
| |||
{
| |||
if(ObjectFind(0, name) < 0)
| |||
{
| |||
if(!ObjectCreate(0, name, OBJ_VLINE, 0, t, 0.0))
| |||
{
| |||
PrintFormat("ICTMacros: ObjectCreate failed for %s (error %d)", name, GetLastError());
| |||
return false;
| |||
}
| |||
}
| |||
else
| |||
ObjectMove(0, name, 0, t, 0.0);
| |||
| |||
SetCommonProps(name, clr, 1, true);
| |||
ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_DOT);
| |||
return true;
| |||
}
| |||
| |||
bool EnsureText(const string name, const datetime t, const double price,
| |||
const string text, const color clr, const int fontSize)
| |||
{
| |||
if(ObjectFind(0, name) < 0)
| |||
{
| |||
if(!ObjectCreate(0, name, OBJ_TEXT, 0, t, price))
| |||
{
| |||
PrintFormat("ICTMacros: ObjectCreate failed for %s (error %d)", name, GetLastError());
| |||
return false;
| |||
}
| |||
}
| |||
else
| |||
ObjectMove(0, name, 0, t, price);
| |||
| |||
SetCommonProps(name, clr, 1, false);
| |||
| |||
int fs = fontSize;
| |||
if(fs < 6) fs = 6;
| |||
if(fs > 20) fs = 20;
| |||
| |||
ObjectSetString(0, name, OBJPROP_TEXT, text);
| |||
ObjectSetString(0, name, OBJPROP_TOOLTIP, text);
| |||
ObjectSetString(0, name, OBJPROP_FONT, "Arial");
| |||
ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fs);
| |||
ObjectSetInteger(0, name, OBJPROP_ANCHOR, ANCHOR_LEFT_LOWER);
| |||
return true;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Object name registry (never touches objects of other programs) |
| |||
//+------------------------------------------------------------------+
| |||
void RegisterName(const string name)
| |||
{
| |||
const int size = ArraySize(g_names);
| |||
if(g_nameCount >= size)
| |||
{
| |||
if(ArrayResize(g_names, size + 64) <= size)
| |||
return; // registry full: keep drawing, skip tracking
| |||
}
| |||
g_names[g_nameCount] = name;
| |||
g_nameCount++;
| |||
}
| |||
| |||
bool NameInList(const string name, string &list[], const int count)
| |||
{
| |||
for(int i = 0; i < count; i++)
| |||
if(list[i] == name)
| |||
return true;
| |||
return false;
| |||
}
| |||
| |||
bool CapacityOk()
| |||
{
| |||
if(g_nameCount + 4 > InpMaxObjects)
| |||
{
| |||
if(!g_capWarned)
| |||
{
| |||
PrintFormat("ICTMacros: object cap InpMaxObjects=%d reached - older windows are skipped", InpMaxObjects);
| |||
g_capWarned = true;
| |||
}
| |||
return false;
| |||
}
| |||
return true;
| |||
}
| |||
| |||
//| Remove ICTMacros_ objects left behind by a former run. Runs once per |
| |||
//| initialization. Disable InpCleanStaleObjects when two copies of this |
| |||
//| indicator share one chart. |
| |||
void SweepOrphanObjects()
| |||
{
| |||
if(!InpCleanStaleObjects || g_orphanSwept)
| |||
return;
| |||
g_orphanSwept = true;
| |||
| |||
const int total = ObjectsTotal(0, -1, -1);
| |||
string doomed[];
| |||
int doomedCount = 0;
| |||
| |||
for(int i = 0; i < total; i++)
| |||
{
| |||
const string name = ObjectName(0, i, -1, -1);
| |||
if(StringFind(name, ICT_PREFIX) != 0) // only this program's namespace
| |||
continue;
| |||
if(NameInList(name, g_names, g_nameCount))
| |||
continue;
| |||
| |||
const int size = ArraySize(doomed);
| |||
if(doomedCount >= size)
| |||
if(ArrayResize(doomed, size + 32) <= size)
| |||
break;
| |||
doomed[doomedCount] = name;
| |||
doomedCount++;
| |||
}
| |||
| |||
for(int i = 0; i < doomedCount; i++)
| |||
ObjectDelete(0, doomed[i]);
| |||
| |||
if(doomedCount > 0)
| |||
PrintFormat("ICTMacros: removed %d leftover object(s) from a previous run", doomedCount);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Drawing |
| |||
//+------------------------------------------------------------------+
| |||
string DateTag(const datetime refDay)
| |||
{
| |||
MqlDateTime d;
| |||
TimeToStruct(refDay, d);
| |||
return StringFormat("%04d%02d%02d", d.year, d.mon, d.day);
| |||
}
| |||
| |||
void DrawWindow(const int wi, const datetime refDay, const datetime tStart, const datetime tEnd,
| |||
const double hi, const double lo)
| |||
{
| |||
const int session = g_win[wi].session;
| |||
const color clr = SessionColor(session);
| |||
const string base = StringFormat("%s%s_%02d_%s",
| |||
ICT_PREFIX, SessionTag(session), g_win[wi].index, DateTag(refDay));
| |||
| |||
if(InpDrawBox || InpFillBox)
| |||
if(EnsureRect(base, tStart, lo, tEnd, hi, clr))
| |||
RegisterName(base);
| |||
| |||
if(InpMidline)
| |||
{
| |||
const string midName = base + "_mid";
| |||
const double mid = (hi + lo) / 2.0;
| |||
if(EnsureTrend(midName, tStart, mid, tEnd, mid, clr))
| |||
RegisterName(midName);
| |||
}
| |||
| |||
if(InpVerticalLines)
| |||
{
| |||
const string startName = base + "_S";
| |||
const string endName = base + "_E";
| |||
if(EnsureVLine(startName, tStart, clr))
| |||
RegisterName(startName);
| |||
if(EnsureVLine(endName, tEnd, clr))
| |||
RegisterName(endName);
| |||
}
| |||
| |||
if(InpShowLabels)
| |||
{
| |||
const string labelName = base + "_L";
| |||
if(EnsureText(labelName, tStart, hi, g_win[wi].label, clr, InpLabelFontSize))
| |||
RegisterName(labelName);
| |||
}
| |||
}
| |||
| |||
//| Full rebuild. Runs on init and on every new bar. |
| |||
void RebuildAll(const datetime &time[], const double &high[], const double &low[], const int rates_total)
| |||
{
| |||
g_nameCount = 0;
| |||
g_capWarned = false;
| |||
g_liveHi = 0.0;
| |||
g_liveLo = 0.0;
| |||
| |||
int days = InpHistoryDays;
| |||
if(days < 0) days = 0;
| |||
if(days > 200) days = 200;
| |||
| |||
const datetime refDay = DayStart(ReferenceNow());
| |||
bool stop = false;
| |||
| |||
for(int d = 0; d <= days && !stop; d++)
| |||
{
| |||
const datetime day = (datetime)((long)refDay - (long)d * ICT_SEC_PER_DAY);
| |||
| |||
for(int w = 0; w < g_winCount && !stop; w++)
| |||
{
| |||
if(!SessionEnabled(g_win[w].session))
| |||
continue;
| |||
if(!CapacityOk())
| |||
{
| |||
stop = true;
| |||
break;
| |||
}
| |||
| |||
const datetime t1 = WindowTimeToServer(day, g_win[w].startMin);
| |||
const datetime t2 = WindowTimeToServer(day, g_win[w].endMin);
| |||
| |||
const int i1 = LowerBound(time, rates_total, t1);
| |||
if(i1 >= rates_total)
| |||
continue; // window has not started yet
| |||
const int i2 = LowerBound(time, rates_total, t2);
| |||
const int last = i2 - 1; // last bar inside the window
| |||
if(last < i1)
| |||
continue; // no bars (weekend / holiday / gap)
| |||
| |||
double hi = high[i1];
| |||
double lo = low[i1];
| |||
for(int i = i1 + 1; i <= last; i++)
| |||
{
| |||
if(high[i] > hi) hi = high[i];
| |||
if(low[i] < lo) lo = low[i];
| |||
}
| |||
| |||
datetime tEnd = (datetime)((long)time[last] + PeriodSeconds());
| |||
if(tEnd > t2)
| |||
tEnd = t2;
| |||
| |||
DrawWindow(w, day, time[i1], tEnd, hi, lo);
| |||
}
| |||
}
| |||
| |||
//--- drop objects of the previous rebuild that are no longer valid
| |||
for(int i = 0; i < g_prevCount; i++)
| |||
if(!NameInList(g_prevNames[i], g_names, g_nameCount))
| |||
ObjectDelete(0, g_prevNames[i]);
| |||
| |||
//--- swap registries
| |||
if(ArrayResize(g_prevNames, g_nameCount) != g_nameCount)
| |||
Print("ICTMacros: could not resize the object registry");
| |||
for(int i = 0; i < g_nameCount; i++)
| |||
g_prevNames[i] = g_names[i];
| |||
g_prevCount = g_nameCount;
| |||
| |||
ChartRedraw();
| |||
}
| |||
| |||
//| Refresh only the window that is currently open (tick path). |
| |||
void RefreshLiveWindow(const datetime &time[], const double &high[], const double &low[], const int rates_total)
| |||
{
| |||
if(!InpLiveBox || rates_total < 2)
| |||
return;
| |||
| |||
const datetime lastBar = time[rates_total - 1];
| |||
const datetime refDay = DayStart(ReferenceNow());
| |||
| |||
for(int w = 0; w < g_winCount; w++)
| |||
{
| |||
if(!SessionEnabled(g_win[w].session))
| |||
continue;
| |||
| |||
const datetime t1 = WindowTimeToServer(refDay, g_win[w].startMin);
| |||
const datetime t2 = WindowTimeToServer(refDay, g_win[w].endMin);
| |||
if(lastBar < t1 || lastBar >= t2)
| |||
continue; // this window is not open right now
| |||
| |||
const int i1 = LowerBound(time, rates_total, t1);
| |||
if(i1 >= rates_total)
| |||
return;
| |||
| |||
double hi = high[i1];
| |||
double lo = low[i1];
| |||
for(int i = i1 + 1; i < rates_total; i++)
| |||
{
| |||
if(high[i] > hi) hi = high[i];
| |||
if(low[i] < lo) lo = low[i];
| |||
}
| |||
if(hi == g_liveHi && lo == g_liveLo)
| |||
return; // extremes unchanged: nothing to do
| |||
| |||
const string base = StringFormat("%s%s_%02d_%s",
| |||
ICT_PREFIX, SessionTag(g_win[w].session), g_win[w].index, DateTag(refDay));
| |||
if(ObjectFind(0, base) < 0)
| |||
return; // box of this window not built yet
| |||
| |||
datetime tEnd = (datetime)((long)lastBar + PeriodSeconds());
| |||
if(tEnd > t2)
| |||
tEnd = t2;
| |||
| |||
ObjectMove(0, base, 0, time[i1], lo);
| |||
ObjectMove(0, base, 1, tEnd, hi);
| |||
| |||
if(InpMidline)
| |||
{
| |||
const string midName = base + "_mid";
| |||
if(ObjectFind(0, midName) >= 0)
| |||
{
| |||
const double mid = (hi + lo) / 2.0;
| |||
ObjectMove(0, midName, 0, time[i1], mid);
| |||
ObjectMove(0, midName, 1, tEnd, mid);
| |||
}
| |||
}
| |||
if(InpShowLabels)
| |||
{
| |||
const string labelName = base + "_L";
| |||
if(ObjectFind(0, labelName) >= 0)
| |||
ObjectMove(0, labelName, 0, time[i1], hi);
| |||
}
| |||
| |||
g_liveHi = hi;
| |||
g_liveLo = lo;
| |||
ChartRedraw();
| |||
return;
| |||
}
| |||
}
| |||
| |||
void LogSummary()
| |||
{
| |||
if(!InpVerboseLog)
| |||
return;
| |||
| |||
int asia = 0, london = 0, ny = 0;
| |||
for(int i = 0; i < g_winCount; i++)
| |||
{
| |||
if(g_win[i].session == ICT_ASIA) asia++;
| |||
else if(g_win[i].session == ICT_LONDON) london++;
| |||
else ny++;
| |||
}
| |||
| |||
PrintFormat("ICTMacros: base=%s server offset=%d min windows=%d (Asia %d, London %d, NY %d) history=%d d objects=%d",
| |||
TimeBaseName(InpTimeBase), g_srvOffsetSec / 60, g_winCount,
| |||
asia, london, ny, InpHistoryDays, g_nameCount);
| |||
| |||
if(InpTimeBase == ICT_TB_NEWYORK)
| |||
{
| |||
const datetime utcNow = (datetime)((long)TimeTradeServer() - (long)g_srvOffsetSec);
| |||
const datetime nyNow = (datetime)((long)utcNow + (long)NyUtcOffsetSec(utcNow));
| |||
string zoneTag = "EST";
| |||
if(IsUsDst(utcNow))
| |||
zoneTag = "EDT";
| |||
PrintFormat("ICTMacros: New York local time now %s (%s)",
| |||
TimeToString(nyNow, TIME_DATE | TIME_MINUTES), zoneTag);
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Indicator events |
| |||
//+------------------------------------------------------------------+
| |||
int OnInit()
| |||
{
| |||
IndicatorSetString(INDICATOR_SHORTNAME, "ICT Macros");
| |||
| |||
ArrayResize(g_win, 0);
| |||
g_winCount = 0;
| |||
g_winCount += ParseWindowList(InpAsiaMacros, ICT_ASIA, g_win);
| |||
g_winCount += ParseWindowList(InpLondonMacros, ICT_LONDON, g_win);
| |||
g_winCount += ParseWindowList(InpNYMacros, ICT_NEWYORK, g_win);
| |||
| |||
g_srvOffsetSec = DetectServerOffsetSec();
| |||
g_lastBarTime = 0;
| |||
g_liveHi = 0.0;
| |||
g_liveLo = 0.0;
| |||
g_loggedSummary = false;
| |||
g_capWarned = false;
| |||
g_orphanSwept = false;
| |||
g_nameCount = 0;
| |||
g_prevCount = 0;
| |||
ArrayResize(g_names, 0);
| |||
ArrayResize(g_prevNames, 0);
| |||
| |||
if(g_winCount == 0)
| |||
Print("ICTMacros: no macro windows configured - nothing will be drawn");
| |||
if(!InpShowAsia && !InpShowLondon && !InpShowNewYork)
| |||
Print("ICTMacros: all sessions are disabled - nothing will be drawn");
| |||
if(g_winCount > 0 && PeriodSeconds() > 1800)
| |||
PrintFormat("ICTMacros: chart period is %d s, macro windows may cover 0 or 1 bar (M1-M15 recommended)",
| |||
PeriodSeconds());
| |||
if(InpTimeBase != ICT_TB_SERVER && InpServerOffsetMode == ICT_SRV_AUTO && InpVerboseLog)
| |||
PrintFormat("ICTMacros: server/UTC offset detected as %d minutes", g_srvOffsetSec / 60);
| |||
| |||
return INIT_SUCCEEDED;
| |||
}
| |||
| |||
void OnDeinit(const int reason)
| |||
{
| |||
for(int i = 0; i < g_nameCount; i++)
| |||
ObjectDelete(0, g_names[i]);
| |||
for(int i = 0; i < g_prevCount; i++)
| |||
ObjectDelete(0, g_prevNames[i]);
| |||
| |||
g_nameCount = 0;
| |||
g_prevCount = 0;
| |||
ArrayResize(g_names, 0);
| |||
ArrayResize(g_prevNames, 0);
| |||
ChartRedraw();
| |||
}
| |||
| |||
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;
| |||
| |||
const bool newBar = (time[rates_total - 1] != g_lastBarTime);
| |||
const bool firstPass = (prev_calculated <= 0) || (prev_calculated > rates_total);
| |||
| |||
if(firstPass || newBar)
| |||
{
| |||
//--- keep the server offset in sync with broker DST switches
| |||
if(InpTimeBase != ICT_TB_SERVER && InpServerOffsetMode == ICT_SRV_AUTO)
| |||
{
| |||
const int detected = DetectServerOffsetSec();
| |||
if(detected != g_srvOffsetSec)
| |||
{
| |||
if(InpVerboseLog)
| |||
PrintFormat("ICTMacros: server/UTC offset changed from %d to %d minutes",
| |||
g_srvOffsetSec / 60, detected / 60);
| |||
g_srvOffsetSec = detected;
| |||
}
| |||
}
| |||
| |||
g_lastBarTime = time[rates_total - 1];
| |||
RebuildAll(time, high, low, rates_total);
| |||
| |||
if(!g_loggedSummary)
| |||
{
| |||
g_loggedSummary = true;
| |||
LogSummary();
| |||
}
| |||
SweepOrphanObjects();
| |||
}
| |||
else
| |||
RefreshLiveWindow(time, high, low, rates_total);
| |||
| |||
return rates_total;
| |||
}
| |||
//+------------------------------------------------------------------+
|