forked from animatedread/Warrior_EA
User design (2026-08-19): 'remove the enum menu that selects neural networks... individual inputs for every NN just like classic signals... the META NN should be integrated into the voting decision pipeline when enabled... as a bonus meta labelling is applied to enabled NNs.' - AI_CHOICE is GONE (tombstoned per the stale-.set doctrine). Use_MLP/Use_CONV/Use_LSTM/ Use_CONVLSTM are ordinary bools like the classic votes; the ensemble arithmetic adapts to any subset because the consensus divisor is the enabled capable weight. Two or more enabled = ensemble (|ENS1 token + joint gate, exactly the old AI_HYBRID fingerprints, so existing weight files keep loading); one = the old solo preset; none = classic-only. - Use_MetaLabeling un-couples META from the direction NNs (the old selector made them mutually exclusive). S3 ships: CSignalMETA::LiveMetaGate scores each vote-cleared entry (shared window at bar 1 + proposal descriptor: side, net vote, live geometry, spread/ATR; pattern one-hot ZEROED - ranking, not calibrated probability, documented in the body) and vetoes below the cost-adjusted break-even. Entries only; fail-open everywhere, loudly. - COEXISTENCE HAZARDS closed: VoteCapableWeight()=0 and ProspectiveVote()=false for the meta target - solo-only until today, a trained META would otherwise sit in the consensus divisor as a permanent abstainer and shrink every vote by its module weight. - CERTIFIED == TRADED: the ensemble era verdict replays the identical veto through the same g_warriorMetaGate pointer over its OOS fired bars (bar re-resolved from the row's own time; fail-open counted as fires and reported: 'metaGate: N approved, M vetoed, K unscored'). The overlay deliberately does NOT replay it (veto-filter-in-replay class, calendar-cliff precedent) - documented at the sweep site. Solo charts' own gate does not model the veto - the standing solo-gate caveat, documented at the input. - DB continuity: the pattern/journal DB fingerprint's first slot was (int)AIType; DbLegacyAiSlot() maps every legacy-expressible config to its OLD value (new 2-3 member subsets get 100+bitmask, outside the legacy range) so no existing database re-keys. filterID becomes the enabled roster via one EnabledNNSummary(). - HUD: the meta line shows the gate (armed/(trn), last P vs BE, ok/veto tally); the armed/disarmed announcement fires on state change via one latch (MetaGateArmedNow), not only when an entry happens to be proposed. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
300 lines
15 KiB
MQL5
300 lines
15 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. OBJ_LABEL also does not render |
|
|
//| embedded '\n' as line breaks (it printed as one truncated line), |
|
|
//| so each line of the status text gets its own OBJ_LABEL, and each |
|
|
//| label gets its own OBJ_RECTANGLE_LABEL background sized to that |
|
|
//| line's actual rendered pixel width via TextGetSize() - not a fixed |
|
|
//| per-character guess, which was oversized on this system's DPI/font |
|
|
//| metrics and left one big box behind the whole block. Some source |
|
|
//| lines are wider than the chart pane itself, which clipped the |
|
|
//| OBJ_LABEL text at the chart edge while its background - sized off |
|
|
//| the full, unclipped string - kept extending to the true width; |
|
|
//| word-wrapping every line to the chart's own pixel width keeps what |
|
|
//| is drawn and what is measured the same string. Separately, |
|
|
//| ObjectSetString(OBJPROP_TEXT) itself silently truncates a label's |
|
|
//| text at ~63 characters regardless of chart width or the pixel wrap |
|
|
//| above - confirmed by several status lines all cutting off almost |
|
|
//| exactly at 63 chars even on a wide chart - so wrapping also |
|
|
//| enforces a hard character cap well under that limit, which is what |
|
|
//| actually keeps every line fully visible. |
|
|
//+------------------------------------------------------------------+
|
|
#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). The AI signals each own a|
|
|
//| full multi-line status panel, and all of them write to the SAME |
|
|
//| global label objects above - so an ensemble chart would flicker |
|
|
//| between four stacked panels, last writer winning each throttle |
|
|
//| tick, covering the chart side (user request 2026-08-15: one |
|
|
//| aggregated panel). Instead, each ensemble member claims a slot |
|
|
//| here and publishes only its HEADLINE (the first line of the panel |
|
|
//| it would have drawn), and the combined render shows one compact |
|
|
//| block: a header plus one line per model. Members' own throttles |
|
|
//| still bound how often they publish; the render below additionally |
|
|
//| skips unchanged text and enforces its own minimum interval so four |
|
|
//| publishers cannot multiply ChartRedraw() cost. |
|
|
//+------------------------------------------------------------------+
|
|
#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. Slots used to be handed out first-come-first-served on each member's
|
|
//--- first PublishStatus() call, which is a race: whichever models were busy sweeping published before
|
|
//--- the ones sitting idle, so XAUUSD rendered LSTM, ConvLSTM, Perceptron, Convolutional instead of
|
|
//--- Perceptron, Convolutional, LSTM, ConvLSTM (2026-08-17). The row is now the member's registration
|
|
//--- index (construction order), which is fixed for the life of the chart, so the panel reads the same
|
|
//--- way every time and a member's line stays where the eye last found it.
|
|
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. Empty until the first synchronized era completes. Declared here (not with the rest
|
|
//--- of the ensemble machinery) because this header is included before ExpertSignalAIBase.mqh and the
|
|
//--- render loop below needs it.
|
|
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);
|
|
}
|