Warrior_EA/System/StatusLabel.mqh
AnimateDread b91c7b1f7a refactor(comments): box headers to stdlib length
The //| box blocks were excluded from 0b06f8e and 5efdb48 and were what
remained: 160 of them ran to 10+ lines, the longest to 88. Compressed to their
leading topic sentences - 5 lines for a function header, 8 for a file header -
keeping the box format and the standard MQL5 name/author lines verbatim.

Verified at the BYTE level this time, across every in-scope file: the list of
non-comment lines is byte-identical to HEAD and braces balance. The first check
compared a locale-decoded 'git show' against a UTF-8 read and flagged 25 files
that had not changed at all - every BOM and every non-ASCII line mismatched.

47,696 -> 40,665 lines in scope; comment share 38% -> 26%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 00:30:14 -04:00

265 lines
12 KiB
MQL5

//+------------------------------------------------------------------+
//| StatusLabel.mqh |
//| AnimateDread |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "AnimateDread"
#property link "https://www.mql5.com"
//+------------------------------------------------------------------+
//| Replaces Comment() for on-chart status text. Comment() draws |
//| directly onto the chart surface with no background, so status |
//| text becomes unreadable over candles. |
//+------------------------------------------------------------------+
#define STATUS_LABEL_PREFIX "WarriorStatusLine"
#define STATUS_LABEL_FONTSIZE 9
#define STATUS_LABEL_FONT "Consolas"
#define STATUS_LABEL_PAD_X 3
#define STATUS_LABEL_PAD_Y 1
#define STATUS_LABEL_LINE_GAP 2 // extra pixels between successive lines' backgrounds
#define STATUS_LABEL_RIGHT_MARGIN 20
#define STATUS_LABEL_MIN_WRAP_WIDTH 150
#define STATUS_LABEL_MAX_CHARS 55 // safe margin under OBJPROP_TEXT's observed ~63-char truncation
#define STATUS_LABEL_MAX_LINES 64
int g_statusLabelLineCount = 0;
//--- Y (chart pixels, CORNER_LEFT_UPPER) just below the last line SetStatusLabel() drew - i.e. where
//--- something else can be placed to sit UNDER the label instead of overlapping it. Defaults to the
//--- label's own default top (see SetStatusLabel()'s y=20 default) before the label has ever drawn.
int g_statusLabelBottomY = 20;
int StatusLabelBottomY(void) { return g_statusLabelBottomY; }
void EnsureStatusLine(int idx)
{
string bg = STATUS_LABEL_PREFIX + (string)idx + "BG";
string txt = STATUS_LABEL_PREFIX + (string)idx + "Text";
if(ObjectFind(0, bg) < 0)
{
ObjectCreate(0, bg, OBJ_RECTANGLE_LABEL, 0, 0, 0);
ObjectSetInteger(0, bg, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, bg, OBJPROP_BGCOLOR, (color)ColorToARGB(clrBlack, 150));
ObjectSetInteger(0, bg, OBJPROP_BORDER_TYPE, BORDER_FLAT);
ObjectSetInteger(0, bg, OBJPROP_COLOR, clrNONE);
ObjectSetInteger(0, bg, OBJPROP_BACK, false);
ObjectSetInteger(0, bg, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, bg, OBJPROP_SELECTED, false);
ObjectSetInteger(0, bg, OBJPROP_HIDDEN, true);
}
if(ObjectFind(0, txt) < 0)
{
ObjectCreate(0, txt, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, txt, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, txt, OBJPROP_ANCHOR, ANCHOR_LEFT_UPPER);
ObjectSetInteger(0, txt, OBJPROP_FONTSIZE, STATUS_LABEL_FONTSIZE);
ObjectSetString(0, txt, OBJPROP_FONT, STATUS_LABEL_FONT);
ObjectSetInteger(0, txt, OBJPROP_COLOR, clrWhite);
ObjectSetInteger(0, txt, OBJPROP_BACK, false);
ObjectSetInteger(0, txt, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, txt, OBJPROP_SELECTED, false);
ObjectSetInteger(0, txt, OBJPROP_HIDDEN, true);
}
}
//+------------------------------------------------------------------+
//| Appends one already-short-enough chunk to outLines/outCount, |
//| force-splitting it further if it still exceeds |
//| STATUS_LABEL_MAX_CHARS (a single word longer than the cap, with |
//| no spaces to break on). |
//+------------------------------------------------------------------+
void AppendStatusChunk(string chunk, string &outLines[], int &outCount)
{
int pos = 0;
int len = StringLen(chunk);
while(len - pos > STATUS_LABEL_MAX_CHARS)
{
ArrayResize(outLines, outCount + 1);
outLines[outCount++] = StringSubstr(chunk, pos, STATUS_LABEL_MAX_CHARS);
pos += STATUS_LABEL_MAX_CHARS;
}
ArrayResize(outLines, outCount + 1);
outLines[outCount++] = StringSubstr(chunk, pos);
}
//+------------------------------------------------------------------+
//| Greedily breaks one logical line into as many visual lines as |
//| needed to keep each one's measured TextGetSize() width within |
//| maxWidth AND its character count within STATUS_LABEL_MAX_CHARS - |
//| the latter is what actually matters (see this file's header |
//| comment on ObjectSetString's ~63-char truncation), the pixel width |
//| check just wraps sooner on a narrow chart. Requires TextSetFont() |
//| to already have been called by the caller so TextGetSize() |
//| measures against the same font actually rendered. |
//+------------------------------------------------------------------+
void WrapLineInto(string line, int maxWidth, string &outLines[], int &outCount)
{
uint dummyW = 0, dummyH = 0;
TextGetSize(line, dummyW, dummyH);
if(StringLen(line) == 0 || ((int)dummyW <= maxWidth && StringLen(line) <= STATUS_LABEL_MAX_CHARS))
{
ArrayResize(outLines, outCount + 1);
outLines[outCount++] = line;
return;
}
string words[];
int wordCount = StringSplit(line, ' ', words);
string current = "";
for(int w = 0; w < wordCount; w++)
{
string candidate = (StringLen(current) == 0) ? words[w] : current + " " + words[w];
uint cw = 0, ch = 0;
TextGetSize(candidate, cw, ch);
bool tooWide = ((int)cw > maxWidth || StringLen(candidate) > STATUS_LABEL_MAX_CHARS);
if(tooWide && StringLen(current) > 0)
{
AppendStatusChunk(current, outLines, outCount);
current = words[w];
}
else
current = candidate;
}
if(StringLen(current) > 0)
AppendStatusChunk(current, outLines, outCount);
}
void SetStatusLabel(string text, int x = 10, int y = 20)
{
//--- TextSetFont's size argument is tenths of a point when negative (matches OBJPROP_FONTSIZE's
//--- point-based units above) - must be set before any TextGetSize() call since it's global state
TextSetFont(STATUS_LABEL_FONT, -STATUS_LABEL_FONTSIZE * 10, 0);
long chartWidthPx = 0;
ChartGetInteger(0, CHART_WIDTH_IN_PIXELS, 0, chartWidthPx);
int maxWidth = (int)chartWidthPx - x - STATUS_LABEL_RIGHT_MARGIN;
if(maxWidth < STATUS_LABEL_MIN_WRAP_WIDTH)
maxWidth = STATUS_LABEL_MIN_WRAP_WIDTH;
string rawLines[];
int rawCount = StringSplit(text, '\n', rawLines);
string lines[];
int lineCount = 0;
for(int r = 0; r < rawCount && lineCount < STATUS_LABEL_MAX_LINES; r++)
WrapLineInto(rawLines[r], maxWidth, lines, lineCount);
if(lineCount > STATUS_LABEL_MAX_LINES)
lineCount = STATUS_LABEL_MAX_LINES;
int lineY = y;
for(int i = 0; i < lineCount; i++)
{
EnsureStatusLine(i);
string bg = STATUS_LABEL_PREFIX + (string)i + "BG";
string txt = STATUS_LABEL_PREFIX + (string)i + "Text";
uint textW = 0, textH = 0;
//--- an empty line measures 0x0, which would collapse its background - fall back to a single
//--- space so blank separator lines still reserve a row of height
TextGetSize(StringLen(lines[i]) > 0 ? lines[i] : " ", textW, textH);
ObjectSetInteger(0, bg, OBJPROP_XDISTANCE, x - STATUS_LABEL_PAD_X);
ObjectSetInteger(0, bg, OBJPROP_YDISTANCE, lineY - STATUS_LABEL_PAD_Y);
ObjectSetInteger(0, bg, OBJPROP_XSIZE, (int)textW + STATUS_LABEL_PAD_X * 2);
ObjectSetInteger(0, bg, OBJPROP_YSIZE, (int)textH + STATUS_LABEL_PAD_Y * 2);
ObjectSetInteger(0, txt, OBJPROP_XDISTANCE, x);
ObjectSetInteger(0, txt, OBJPROP_YDISTANCE, lineY);
ObjectSetString(0, txt, OBJPROP_TEXT, lines[i]);
lineY += (int)textH + STATUS_LABEL_LINE_GAP;
}
g_statusLabelBottomY = lineY;
//--- fewer lines than last call - remove the now-unused trailing line objects rather than
//--- leaving stale text (e.g. a previous longer status block) hanging below the new one
for(int i = lineCount; i < g_statusLabelLineCount; i++)
{
string bg = STATUS_LABEL_PREFIX + (string)i + "BG";
string txt = STATUS_LABEL_PREFIX + (string)i + "Text";
if(ObjectFind(0, bg) >= 0)
ObjectDelete(0, bg);
if(ObjectFind(0, txt) >= 0)
ObjectDelete(0, txt);
}
g_statusLabelLineCount = lineCount;
//--- ObjectSet*() alone does not trigger a repaint - without this the new text/background only
//--- appears once something else forces a redraw (dragging the chart, clicking an object, a new
//--- bar), which made updates look stalled between those events
ChartRedraw(0);
}
//+------------------------------------------------------------------+
//| Removes every status line object. Mirrors the old Comment("") |
//| clear on shutdown - ObjectsDeleteAll(0) elsewhere clears chart |
//| objects, but callers still need an explicit clear at points where |
//| only the status text (not the whole chart) should be reset. |
//+------------------------------------------------------------------+
void ClearStatusLabel(void)
{
for(int i = 0; i < g_statusLabelLineCount; i++)
{
string bg = STATUS_LABEL_PREFIX + (string)i + "BG";
string txt = STATUS_LABEL_PREFIX + (string)i + "Text";
if(ObjectFind(0, bg) >= 0)
ObjectDelete(0, bg);
if(ObjectFind(0, txt) >= 0)
ObjectDelete(0, txt);
}
g_statusLabelLineCount = 0;
ChartRedraw(0);
}
//+------------------------------------------------------------------+
//| ENSEMBLE PANEL (two or more NNs enabled). |
//+------------------------------------------------------------------+
#define ENSEMBLE_PANEL_MAX_MEMBERS 6
string g_ensPanelTag[ENSEMBLE_PANEL_MAX_MEMBERS];
string g_ensPanelLine[ENSEMBLE_PANEL_MAX_MEMBERS];
//--- ROW OWNERSHIP, so the panel is ordered by the ENSEMBLE's own member order and not by which
//--- member happened to publish first.
bool g_ensPanelSlotUsed[ENSEMBLE_PANEL_MAX_MEMBERS];
int g_ensPanelUsed = 0;
uint g_ensPanelLastRender = 0;
string g_ensPanelLastText = "";
//--- The ensemble's COMBINED-vote OOS score line, written once per era by the last member to finish
//--- its pass-3 scan (CExpertSignalAIBase::EnsembleScoreCombinedVote) and rendered under the member
//--- lines below.
string g_ensembleVoteLine = "";
//--- index = the caller's ensemble registration index. Idempotent: safe to call on every publish, which
//--- is what keeps the tag current for a member whose ID was not final when it first claimed its row.
int ClaimEnsemblePanelSlot(string tag, int index)
{
int slot = index;
//--- Unregistered or out of range (should not happen for a real member): fall back to the first free
//--- row rather than dropping the model off the panel entirely.
if(slot < 0 || slot >= ENSEMBLE_PANEL_MAX_MEMBERS)
{
slot = -1;
for(int i = 0; i < ENSEMBLE_PANEL_MAX_MEMBERS; i++)
if(!g_ensPanelSlotUsed[i])
{
slot = i;
break;
}
if(slot < 0)
slot = ENSEMBLE_PANEL_MAX_MEMBERS - 1;
}
if(!g_ensPanelSlotUsed[slot])
{
g_ensPanelSlotUsed[slot] = true;
g_ensPanelLine[slot] = "";
}
if(tag != "")
g_ensPanelTag[slot] = tag;
//--- High-water mark of CLAIMED rows, not a running count - rows are addressed by index now, so a
//--- member that has not published yet leaves a gap the render skips rather than shifting everyone up.
if(slot + 1 > g_ensPanelUsed)
g_ensPanelUsed = slot + 1;
return slot;
}
void PublishEnsembleStatus(int slot, string headline, bool force = false)
{
if(slot < 0 || slot >= g_ensPanelUsed || !g_ensPanelSlotUsed[slot])
return;
g_ensPanelLine[slot] = headline;
int shown = 0;
for(int i = 0; i < g_ensPanelUsed; i++)
if(g_ensPanelSlotUsed[i])
shown++;
string s = "AI ensemble - " + (string)shown + " independent models, one line each";
for(int i = 0; i < g_ensPanelUsed; i++)
if(g_ensPanelSlotUsed[i])
s += "\n" + g_ensPanelTag[i] + ": " + (g_ensPanelLine[i] == "" ? "starting..." : g_ensPanelLine[i]);
if(g_ensembleVoteLine != "")
s += "\n" + g_ensembleVoteLine;
//--- unchanged text needs no relayout; changed text still respects a minimum redraw interval
if(s == g_ensPanelLastText)
return;
uint now = GetTickCount();
if(!force && g_ensPanelLastRender != 0 && now - g_ensPanelLastRender < 300)
return;
g_ensPanelLastText = s;
g_ensPanelLastRender = now;
SetStatusLabel(s);
}