//+------------------------------------------------------------------+ //| 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); }