2026-07-17 21:28:59 -04:00
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| 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 |
|
2026-07-17 21:32:55 -04:00
|
|
|
//| 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), |
|
2026-07-17 21:53:09 -04:00
|
|
|
//| 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. |
|
2026-07-17 21:28:59 -04:00
|
|
|
//+------------------------------------------------------------------+
|
2026-07-17 21:36:44 -04:00
|
|
|
#define STATUS_LABEL_PREFIX "WarriorStatusLine"
|
|
|
|
|
#define STATUS_LABEL_FONTSIZE 9
|
|
|
|
|
#define STATUS_LABEL_FONT "Consolas"
|
2026-07-17 21:53:09 -04:00
|
|
|
#define STATUS_LABEL_PAD_X 3
|
|
|
|
|
#define STATUS_LABEL_PAD_Y 1
|
|
|
|
|
#define STATUS_LABEL_LINE_GAP 2 // extra pixels between successive lines' backgrounds
|
2026-07-17 21:36:44 -04:00
|
|
|
#define STATUS_LABEL_RIGHT_MARGIN 20
|
|
|
|
|
#define STATUS_LABEL_MIN_WRAP_WIDTH 150
|
2026-07-17 21:49:30 -04:00
|
|
|
#define STATUS_LABEL_MAX_CHARS 55 // safe margin under OBJPROP_TEXT's observed ~63-char truncation
|
2026-07-17 21:32:55 -04:00
|
|
|
#define STATUS_LABEL_MAX_LINES 64
|
|
|
|
|
int g_statusLabelLineCount = 0;
|
2026-08-16 21:08:41 -04:00
|
|
|
//--- 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; }
|
2026-07-17 21:49:30 -04:00
|
|
|
void EnsureStatusLine(int idx)
|
|
|
|
|
{
|
2026-07-17 21:53:09 -04:00
|
|
|
string bg = STATUS_LABEL_PREFIX + (string)idx + "BG";
|
2026-07-17 21:49:30 -04:00
|
|
|
string txt = STATUS_LABEL_PREFIX + (string)idx + "Text";
|
2026-07-17 21:53:09 -04:00
|
|
|
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);
|
|
|
|
|
}
|
2026-07-17 21:32:55 -04:00
|
|
|
if(ObjectFind(0, txt) < 0)
|
2026-07-17 21:28:59 -04:00
|
|
|
{
|
2026-07-17 21:32:55 -04:00
|
|
|
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);
|
2026-07-17 21:36:44 -04:00
|
|
|
ObjectSetString(0, txt, OBJPROP_FONT, STATUS_LABEL_FONT);
|
2026-07-17 21:32:55 -04:00
|
|
|
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);
|
2026-07-17 21:28:59 -04:00
|
|
|
}
|
2026-07-17 21:32:55 -04:00
|
|
|
}
|
2026-07-17 21:36:44 -04:00
|
|
|
//+------------------------------------------------------------------+
|
2026-07-17 21:49:30 -04:00
|
|
|
//| 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);
|
|
|
|
|
}
|
|
|
|
|
//+------------------------------------------------------------------+
|
2026-07-17 21:36:44 -04:00
|
|
|
//| Greedily breaks one logical line into as many visual lines as |
|
|
|
|
|
//| needed to keep each one's measured TextGetSize() width within |
|
2026-07-17 21:49:30 -04:00
|
|
|
//| 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. |
|
2026-07-17 21:36:44 -04:00
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
void WrapLineInto(string line, int maxWidth, string &outLines[], int &outCount)
|
|
|
|
|
{
|
2026-07-17 21:37:52 -04:00
|
|
|
uint dummyW = 0, dummyH = 0;
|
2026-07-17 21:36:44 -04:00
|
|
|
TextGetSize(line, dummyW, dummyH);
|
2026-07-17 21:49:30 -04:00
|
|
|
if(StringLen(line) == 0 || ((int)dummyW <= maxWidth && StringLen(line) <= STATUS_LABEL_MAX_CHARS))
|
2026-07-17 21:36:44 -04:00
|
|
|
{
|
|
|
|
|
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];
|
2026-07-17 21:37:52 -04:00
|
|
|
uint cw = 0, ch = 0;
|
2026-07-17 21:36:44 -04:00
|
|
|
TextGetSize(candidate, cw, ch);
|
2026-07-17 21:49:30 -04:00
|
|
|
bool tooWide = ((int)cw > maxWidth || StringLen(candidate) > STATUS_LABEL_MAX_CHARS);
|
|
|
|
|
if(tooWide && StringLen(current) > 0)
|
2026-07-17 21:36:44 -04:00
|
|
|
{
|
2026-07-17 21:49:30 -04:00
|
|
|
AppendStatusChunk(current, outLines, outCount);
|
2026-07-17 21:36:44 -04:00
|
|
|
current = words[w];
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
current = candidate;
|
|
|
|
|
}
|
2026-07-17 21:49:30 -04:00
|
|
|
if(StringLen(current) > 0)
|
|
|
|
|
AppendStatusChunk(current, outLines, outCount);
|
2026-07-17 21:36:44 -04:00
|
|
|
}
|
2026-07-17 21:32:55 -04:00
|
|
|
void SetStatusLabel(string text, int x = 10, int y = 20)
|
|
|
|
|
{
|
2026-07-17 21:36:44 -04:00
|
|
|
//--- 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);
|
2026-07-17 21:32:55 -04:00
|
|
|
string lines[];
|
2026-07-17 21:36:44 -04:00
|
|
|
int lineCount = 0;
|
|
|
|
|
for(int r = 0; r < rawCount && lineCount < STATUS_LABEL_MAX_LINES; r++)
|
|
|
|
|
WrapLineInto(rawLines[r], maxWidth, lines, lineCount);
|
2026-07-17 21:32:55 -04:00
|
|
|
if(lineCount > STATUS_LABEL_MAX_LINES)
|
|
|
|
|
lineCount = STATUS_LABEL_MAX_LINES;
|
2026-07-17 21:36:44 -04:00
|
|
|
int lineY = y;
|
2026-07-17 21:32:55 -04:00
|
|
|
for(int i = 0; i < lineCount; i++)
|
2026-07-17 21:28:59 -04:00
|
|
|
{
|
2026-07-17 21:32:55 -04:00
|
|
|
EnsureStatusLine(i);
|
2026-07-17 21:53:09 -04:00
|
|
|
string bg = STATUS_LABEL_PREFIX + (string)i + "BG";
|
2026-07-17 21:32:55 -04:00
|
|
|
string txt = STATUS_LABEL_PREFIX + (string)i + "Text";
|
2026-07-17 21:37:52 -04:00
|
|
|
uint textW = 0, textH = 0;
|
2026-07-17 21:53:09 -04:00
|
|
|
//--- 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
|
2026-07-17 21:36:44 -04:00
|
|
|
TextGetSize(StringLen(lines[i]) > 0 ? lines[i] : " ", textW, textH);
|
2026-07-17 21:53:09 -04:00
|
|
|
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);
|
2026-07-17 21:32:55 -04:00
|
|
|
ObjectSetInteger(0, txt, OBJPROP_XDISTANCE, x);
|
|
|
|
|
ObjectSetInteger(0, txt, OBJPROP_YDISTANCE, lineY);
|
|
|
|
|
ObjectSetString(0, txt, OBJPROP_TEXT, lines[i]);
|
2026-07-17 21:37:52 -04:00
|
|
|
lineY += (int)textH + STATUS_LABEL_LINE_GAP;
|
2026-07-17 21:28:59 -04:00
|
|
|
}
|
2026-08-16 21:08:41 -04:00
|
|
|
g_statusLabelBottomY = lineY;
|
2026-07-17 21:32:55 -04:00
|
|
|
//--- 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++)
|
2026-07-17 21:28:59 -04:00
|
|
|
{
|
2026-07-17 21:53:09 -04:00
|
|
|
string bg = STATUS_LABEL_PREFIX + (string)i + "BG";
|
2026-07-17 21:32:55 -04:00
|
|
|
string txt = STATUS_LABEL_PREFIX + (string)i + "Text";
|
2026-07-17 21:53:09 -04:00
|
|
|
if(ObjectFind(0, bg) >= 0)
|
|
|
|
|
ObjectDelete(0, bg);
|
2026-07-17 21:32:55 -04:00
|
|
|
if(ObjectFind(0, txt) >= 0)
|
|
|
|
|
ObjectDelete(0, txt);
|
2026-07-17 21:28:59 -04:00
|
|
|
}
|
2026-07-17 21:32:55 -04:00
|
|
|
g_statusLabelLineCount = lineCount;
|
2026-07-17 21:36:44 -04:00
|
|
|
//--- 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);
|
2026-07-17 21:28:59 -04:00
|
|
|
}
|
|
|
|
|
//+------------------------------------------------------------------+
|
2026-07-17 21:32:55 -04:00
|
|
|
//| 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. |
|
2026-07-17 21:28:59 -04:00
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
void ClearStatusLabel(void)
|
|
|
|
|
{
|
2026-07-17 21:32:55 -04:00
|
|
|
for(int i = 0; i < g_statusLabelLineCount; i++)
|
|
|
|
|
{
|
2026-07-17 21:53:09 -04:00
|
|
|
string bg = STATUS_LABEL_PREFIX + (string)i + "BG";
|
2026-07-17 21:32:55 -04:00
|
|
|
string txt = STATUS_LABEL_PREFIX + (string)i + "Text";
|
2026-07-17 21:53:09 -04:00
|
|
|
if(ObjectFind(0, bg) >= 0)
|
|
|
|
|
ObjectDelete(0, bg);
|
2026-07-17 21:32:55 -04:00
|
|
|
if(ObjectFind(0, txt) >= 0)
|
|
|
|
|
ObjectDelete(0, txt);
|
|
|
|
|
}
|
|
|
|
|
g_statusLabelLineCount = 0;
|
2026-07-17 21:36:44 -04:00
|
|
|
ChartRedraw(0);
|
2026-07-17 21:28:59 -04:00
|
|
|
}
|
2026-08-15 16:54:43 -04:00
|
|
|
//+------------------------------------------------------------------+
|
feat(ensemble): per-NN inputs replace the preset selector - the meta head becomes the vote's gate
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>
2026-08-19 13:01:02 -04:00
|
|
|
//| ENSEMBLE PANEL (two or more NNs enabled). The AI signals each own a|
|
2026-08-15 16:54:43 -04:00
|
|
|
//| 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];
|
fix(indicators+panel): the dead handle is MEASURED now - recreate it; and order the ensemble panel by member, not by who published first
THE ANSWER, off the instrumentation added in be39674, first run:
ConvLSTM [HYB-2484]: TUNABLE INDICATOR REPORTS NO CALCULATED BARS - 1 tunable
indicator(s) enabled and the least-ready answers BarsCalculated()=-1 ...
Per-indicator depth: price=33982 MA=-1 ZigZag=33982 ATR=33982
MA=-1 with price, ZigZag and ATR all at full depth. **The handle is INVALID, not
short.** Same line on USDJPY (price=50179 MA=-1). Depth was never the problem;
the previous session's five theories were all answering the wrong question.
And it is per-member, not per-chart: LSTM-2484 ran the 34-candidate auto-tune on
that same chart at 15:24:18 and went on to train normally (feature health, 51
features, excursion head) reading the same indicator. Only ConvLSTM's handle -
the last member constructed - was dead. WHY is still not established. All four
members request ADMovingAverage with identical params, so MT5 hands them the SAME
refcounted handle, and the tuner's inner loop is Create-then-IndicatorRelease over
exactly that shared handle; that is the obvious suspect and it is NOT yet proven,
so this commit does not act on it.
1. IndicatorDepthReport() NOW PRINTS HANDLE NUMBERS, not just depths.
"MA=-1" says the handle is dead. "MA=-1(h12)" against another member's "MA=33982
(h12)" says it is the SAME handle and someone released it; "(h-1)" says it was
never created. That is the difference between a refcount bug and a creation
failure and it is one field. This is the measurement the shared-handle suspicion
needs before anyone acts on it.
2. RECREATE A DEAD HANDLE INSTEAD OF SWEEPING AGAINST IT.
A member that cannot read its own indicator must rebuild it. RepairDeadIndicatorHandles()
re-Creates only the ENABLED tunables reporting BarsCalculated() < 0 - a merely COLD
indicator (valid handle, 0 bars) is left alone to warm up the normal way. It does
NOT release first: -1 means the terminal no longer knows the handle, so there is
nothing to give back, and MT5 recycles handle VALUES so releasing a stale one could
decrement whatever now owns that number. 30s cooldown, because every ServableBars()
consumer reaches it including live inference on every tick. The feature cache is
dropped with it, and the log names before/after depths.
Cause-agnostic on purpose. Whatever is killing the handle, sweeping 50,163 bars
against a buffer that answers EMPTY_VALUE at every index - then discarding the era
and doing it again - is not a recovery.
3. THE SWEEP NOW HOLDS ON A DEAD HANDLE.
ServableBars() keeps answering `want` (its contract; live inference and online
learning have their own refusal paths and a 0 there reads as "no history at all").
SettledBars() - the training sweep's entry, the one caller that can afford to wait -
returns 0 instead, so Train() holds and reports rather than burning a full-history
pass it is guaranteed to throw away. A recreated handle is cold, so it primes
through the existing settle path on the next call. If the repair fails the member
holds indefinitely and says so every minute, and be39674's barrier liveness escape
releases the rest of the ensemble after 12 minutes - which is the correct
degradation and is exactly what the log shows happening.
4. THE PANEL ROWS WERE ORDERED BY WHO PUBLISHED FIRST.
Reported on XAUUSD: LSTM, ConvLSTM, Perceptron, Convolutional instead of
Perceptron, Convolutional, LSTM, ConvLSTM. ClaimEnsemblePanelSlot() handed out the
next free row on each member's FIRST PublishStatus() call, so the order was a race -
the members busy sweeping published before the ones sitting idle at the era barrier,
and be39674 sharpened it by (correctly) making a held member stop writing the terse
line. Rows are now keyed to m_ensembleIndex, the registration/construction order,
which is fixed for the life of the chart. Claimed on every publish rather than once,
so it is idempotent and refreshes the tag for a member whose ID was not final when it
first published (the config-tag suffix is appended during InitIndicators, after
EnsembleMember() registers). Unclaimed rows are skipped by the render and excluded
from the model count, so a member that has not published yet leaves no gap and shifts
nobody.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 15:40:00 -04:00
|
|
|
//--- 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];
|
2026-08-15 16:54:43 -04:00
|
|
|
int g_ensPanelUsed = 0;
|
|
|
|
|
uint g_ensPanelLastRender = 0;
|
|
|
|
|
string g_ensPanelLastText = "";
|
fix(ensemble): responsive panel + synchronized eras + combined-vote accuracy
Four user-reported/requested items, one root cause chain:
1) DEAD CONTROL PANEL in AI_HYBRID mode. All members posted custom event
id 1 and handled id 1001, and CExpertCustom broadcasts every chart
event to every filter - so each posted event ran a train chunk in ALL
N members (N*N chunks per round) and the chart thread never idled
long enough to deliver clicks/drags. profiling.csv: 99.45% of time in
OnChartEventHandler. Fix: per-instance study-event ids
(STUDY_EVENT_ID_BASE + construction order, offset above the Controls
library's ON_* codes - id 1 was also ON_DBL_CLICK, so panel
double-clicks fired training chunks). ArmStudyEvent() is the single
post site; lost-event watchdog replaces the accidental
sibling-clears-my-flag rescue.
2) WARM-UP DUPLICATION. The auto-tune sweep is deterministic over
identical features/labels, and it ends in the full MI diagnostic
suite, which the MI-share gate never intercepted on the sweep path -
four members ran four identical ~36s sweep+report blocks. First
member publishes outcome (g_ensembleChartTuneDone/Installed/Settings);
the rest apply it and skip both.
3) DEINIT STRANDED PANEL+ARROWS (user repro 18:52). Root cause from the
log: the 4,500ms budget runs from MetaTrader's stop REQUEST - a heavy
autosave in flight ate it, OnDeinit got ~430ms and died in the first
member's arrow persist ("Abnormal termination" 432ms in). Fix: early
visible-UI sweep (native prefix deletes for status/panel/dialog)
right after ClearStatusLabel, and a fast path for still-training
models - their arrows are re-rendered every era, so they get one bulk
purge instead of scan+atomic-write in the death window.
4) ENSEMBLE FEATURES (user requests): era BARRIER - members advance era
by era together; a member ahead of the slowest still-training member
declines Train() calls and its chunk budget is donated
(TRAIN_TIME_BUDGET_MS = 120/activeTrainers, UI headroom constant).
COMBINED-VOTE OOS SCORE - each member's pass-3 scan contributes its
adjusted per-bar decision (0.0 on abstain) to a shared row buffer;
the last member to finish the era scores the averaged vote vs the
mirrored Min_Vote_Open against the same target-before-stop outcomes
members grade themselves on, publishing an "Ensemble vote" line on
the aggregated panel. Member headlines now carry their lifetime win
rate with break-even.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:06:04 -04:00
|
|
|
//--- 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 = "";
|
fix(indicators+panel): the dead handle is MEASURED now - recreate it; and order the ensemble panel by member, not by who published first
THE ANSWER, off the instrumentation added in be39674, first run:
ConvLSTM [HYB-2484]: TUNABLE INDICATOR REPORTS NO CALCULATED BARS - 1 tunable
indicator(s) enabled and the least-ready answers BarsCalculated()=-1 ...
Per-indicator depth: price=33982 MA=-1 ZigZag=33982 ATR=33982
MA=-1 with price, ZigZag and ATR all at full depth. **The handle is INVALID, not
short.** Same line on USDJPY (price=50179 MA=-1). Depth was never the problem;
the previous session's five theories were all answering the wrong question.
And it is per-member, not per-chart: LSTM-2484 ran the 34-candidate auto-tune on
that same chart at 15:24:18 and went on to train normally (feature health, 51
features, excursion head) reading the same indicator. Only ConvLSTM's handle -
the last member constructed - was dead. WHY is still not established. All four
members request ADMovingAverage with identical params, so MT5 hands them the SAME
refcounted handle, and the tuner's inner loop is Create-then-IndicatorRelease over
exactly that shared handle; that is the obvious suspect and it is NOT yet proven,
so this commit does not act on it.
1. IndicatorDepthReport() NOW PRINTS HANDLE NUMBERS, not just depths.
"MA=-1" says the handle is dead. "MA=-1(h12)" against another member's "MA=33982
(h12)" says it is the SAME handle and someone released it; "(h-1)" says it was
never created. That is the difference between a refcount bug and a creation
failure and it is one field. This is the measurement the shared-handle suspicion
needs before anyone acts on it.
2. RECREATE A DEAD HANDLE INSTEAD OF SWEEPING AGAINST IT.
A member that cannot read its own indicator must rebuild it. RepairDeadIndicatorHandles()
re-Creates only the ENABLED tunables reporting BarsCalculated() < 0 - a merely COLD
indicator (valid handle, 0 bars) is left alone to warm up the normal way. It does
NOT release first: -1 means the terminal no longer knows the handle, so there is
nothing to give back, and MT5 recycles handle VALUES so releasing a stale one could
decrement whatever now owns that number. 30s cooldown, because every ServableBars()
consumer reaches it including live inference on every tick. The feature cache is
dropped with it, and the log names before/after depths.
Cause-agnostic on purpose. Whatever is killing the handle, sweeping 50,163 bars
against a buffer that answers EMPTY_VALUE at every index - then discarding the era
and doing it again - is not a recovery.
3. THE SWEEP NOW HOLDS ON A DEAD HANDLE.
ServableBars() keeps answering `want` (its contract; live inference and online
learning have their own refusal paths and a 0 there reads as "no history at all").
SettledBars() - the training sweep's entry, the one caller that can afford to wait -
returns 0 instead, so Train() holds and reports rather than burning a full-history
pass it is guaranteed to throw away. A recreated handle is cold, so it primes
through the existing settle path on the next call. If the repair fails the member
holds indefinitely and says so every minute, and be39674's barrier liveness escape
releases the rest of the ensemble after 12 minutes - which is the correct
degradation and is exactly what the log shows happening.
4. THE PANEL ROWS WERE ORDERED BY WHO PUBLISHED FIRST.
Reported on XAUUSD: LSTM, ConvLSTM, Perceptron, Convolutional instead of
Perceptron, Convolutional, LSTM, ConvLSTM. ClaimEnsemblePanelSlot() handed out the
next free row on each member's FIRST PublishStatus() call, so the order was a race -
the members busy sweeping published before the ones sitting idle at the era barrier,
and be39674 sharpened it by (correctly) making a held member stop writing the terse
line. Rows are now keyed to m_ensembleIndex, the registration/construction order,
which is fixed for the life of the chart. Claimed on every publish rather than once,
so it is idempotent and refreshes the tag for a member whose ID was not final when it
first published (the config-tag suffix is appended during InitIndicators, after
EnsembleMember() registers). Unclaimed rows are skipped by the render and excluded
from the model count, so a member that has not published yet leaves no gap and shifts
nobody.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 15:40:00 -04:00
|
|
|
//--- 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)
|
2026-08-15 16:54:43 -04:00
|
|
|
{
|
fix(indicators+panel): the dead handle is MEASURED now - recreate it; and order the ensemble panel by member, not by who published first
THE ANSWER, off the instrumentation added in be39674, first run:
ConvLSTM [HYB-2484]: TUNABLE INDICATOR REPORTS NO CALCULATED BARS - 1 tunable
indicator(s) enabled and the least-ready answers BarsCalculated()=-1 ...
Per-indicator depth: price=33982 MA=-1 ZigZag=33982 ATR=33982
MA=-1 with price, ZigZag and ATR all at full depth. **The handle is INVALID, not
short.** Same line on USDJPY (price=50179 MA=-1). Depth was never the problem;
the previous session's five theories were all answering the wrong question.
And it is per-member, not per-chart: LSTM-2484 ran the 34-candidate auto-tune on
that same chart at 15:24:18 and went on to train normally (feature health, 51
features, excursion head) reading the same indicator. Only ConvLSTM's handle -
the last member constructed - was dead. WHY is still not established. All four
members request ADMovingAverage with identical params, so MT5 hands them the SAME
refcounted handle, and the tuner's inner loop is Create-then-IndicatorRelease over
exactly that shared handle; that is the obvious suspect and it is NOT yet proven,
so this commit does not act on it.
1. IndicatorDepthReport() NOW PRINTS HANDLE NUMBERS, not just depths.
"MA=-1" says the handle is dead. "MA=-1(h12)" against another member's "MA=33982
(h12)" says it is the SAME handle and someone released it; "(h-1)" says it was
never created. That is the difference between a refcount bug and a creation
failure and it is one field. This is the measurement the shared-handle suspicion
needs before anyone acts on it.
2. RECREATE A DEAD HANDLE INSTEAD OF SWEEPING AGAINST IT.
A member that cannot read its own indicator must rebuild it. RepairDeadIndicatorHandles()
re-Creates only the ENABLED tunables reporting BarsCalculated() < 0 - a merely COLD
indicator (valid handle, 0 bars) is left alone to warm up the normal way. It does
NOT release first: -1 means the terminal no longer knows the handle, so there is
nothing to give back, and MT5 recycles handle VALUES so releasing a stale one could
decrement whatever now owns that number. 30s cooldown, because every ServableBars()
consumer reaches it including live inference on every tick. The feature cache is
dropped with it, and the log names before/after depths.
Cause-agnostic on purpose. Whatever is killing the handle, sweeping 50,163 bars
against a buffer that answers EMPTY_VALUE at every index - then discarding the era
and doing it again - is not a recovery.
3. THE SWEEP NOW HOLDS ON A DEAD HANDLE.
ServableBars() keeps answering `want` (its contract; live inference and online
learning have their own refusal paths and a 0 there reads as "no history at all").
SettledBars() - the training sweep's entry, the one caller that can afford to wait -
returns 0 instead, so Train() holds and reports rather than burning a full-history
pass it is guaranteed to throw away. A recreated handle is cold, so it primes
through the existing settle path on the next call. If the repair fails the member
holds indefinitely and says so every minute, and be39674's barrier liveness escape
releases the rest of the ensemble after 12 minutes - which is the correct
degradation and is exactly what the log shows happening.
4. THE PANEL ROWS WERE ORDERED BY WHO PUBLISHED FIRST.
Reported on XAUUSD: LSTM, ConvLSTM, Perceptron, Convolutional instead of
Perceptron, Convolutional, LSTM, ConvLSTM. ClaimEnsemblePanelSlot() handed out the
next free row on each member's FIRST PublishStatus() call, so the order was a race -
the members busy sweeping published before the ones sitting idle at the era barrier,
and be39674 sharpened it by (correctly) making a held member stop writing the terse
line. Rows are now keyed to m_ensembleIndex, the registration/construction order,
which is fixed for the life of the chart. Claimed on every publish rather than once,
so it is idempotent and refreshes the tag for a member whose ID was not final when it
first published (the config-tag suffix is appended during InitIndicators, after
EnsembleMember() registers). Unclaimed rows are skipped by the render and excluded
from the model count, so a member that has not published yet leaves no gap and shifts
nobody.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 15:40:00 -04:00
|
|
|
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;
|
2026-08-15 16:54:43 -04:00
|
|
|
}
|
|
|
|
|
void PublishEnsembleStatus(int slot, string headline, bool force = false)
|
|
|
|
|
{
|
fix(indicators+panel): the dead handle is MEASURED now - recreate it; and order the ensemble panel by member, not by who published first
THE ANSWER, off the instrumentation added in be39674, first run:
ConvLSTM [HYB-2484]: TUNABLE INDICATOR REPORTS NO CALCULATED BARS - 1 tunable
indicator(s) enabled and the least-ready answers BarsCalculated()=-1 ...
Per-indicator depth: price=33982 MA=-1 ZigZag=33982 ATR=33982
MA=-1 with price, ZigZag and ATR all at full depth. **The handle is INVALID, not
short.** Same line on USDJPY (price=50179 MA=-1). Depth was never the problem;
the previous session's five theories were all answering the wrong question.
And it is per-member, not per-chart: LSTM-2484 ran the 34-candidate auto-tune on
that same chart at 15:24:18 and went on to train normally (feature health, 51
features, excursion head) reading the same indicator. Only ConvLSTM's handle -
the last member constructed - was dead. WHY is still not established. All four
members request ADMovingAverage with identical params, so MT5 hands them the SAME
refcounted handle, and the tuner's inner loop is Create-then-IndicatorRelease over
exactly that shared handle; that is the obvious suspect and it is NOT yet proven,
so this commit does not act on it.
1. IndicatorDepthReport() NOW PRINTS HANDLE NUMBERS, not just depths.
"MA=-1" says the handle is dead. "MA=-1(h12)" against another member's "MA=33982
(h12)" says it is the SAME handle and someone released it; "(h-1)" says it was
never created. That is the difference between a refcount bug and a creation
failure and it is one field. This is the measurement the shared-handle suspicion
needs before anyone acts on it.
2. RECREATE A DEAD HANDLE INSTEAD OF SWEEPING AGAINST IT.
A member that cannot read its own indicator must rebuild it. RepairDeadIndicatorHandles()
re-Creates only the ENABLED tunables reporting BarsCalculated() < 0 - a merely COLD
indicator (valid handle, 0 bars) is left alone to warm up the normal way. It does
NOT release first: -1 means the terminal no longer knows the handle, so there is
nothing to give back, and MT5 recycles handle VALUES so releasing a stale one could
decrement whatever now owns that number. 30s cooldown, because every ServableBars()
consumer reaches it including live inference on every tick. The feature cache is
dropped with it, and the log names before/after depths.
Cause-agnostic on purpose. Whatever is killing the handle, sweeping 50,163 bars
against a buffer that answers EMPTY_VALUE at every index - then discarding the era
and doing it again - is not a recovery.
3. THE SWEEP NOW HOLDS ON A DEAD HANDLE.
ServableBars() keeps answering `want` (its contract; live inference and online
learning have their own refusal paths and a 0 there reads as "no history at all").
SettledBars() - the training sweep's entry, the one caller that can afford to wait -
returns 0 instead, so Train() holds and reports rather than burning a full-history
pass it is guaranteed to throw away. A recreated handle is cold, so it primes
through the existing settle path on the next call. If the repair fails the member
holds indefinitely and says so every minute, and be39674's barrier liveness escape
releases the rest of the ensemble after 12 minutes - which is the correct
degradation and is exactly what the log shows happening.
4. THE PANEL ROWS WERE ORDERED BY WHO PUBLISHED FIRST.
Reported on XAUUSD: LSTM, ConvLSTM, Perceptron, Convolutional instead of
Perceptron, Convolutional, LSTM, ConvLSTM. ClaimEnsemblePanelSlot() handed out the
next free row on each member's FIRST PublishStatus() call, so the order was a race -
the members busy sweeping published before the ones sitting idle at the era barrier,
and be39674 sharpened it by (correctly) making a held member stop writing the terse
line. Rows are now keyed to m_ensembleIndex, the registration/construction order,
which is fixed for the life of the chart. Claimed on every publish rather than once,
so it is idempotent and refreshes the tag for a member whose ID was not final when it
first published (the config-tag suffix is appended during InitIndicators, after
EnsembleMember() registers). Unclaimed rows are skipped by the render and excluded
from the model count, so a member that has not published yet leaves no gap and shifts
nobody.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 15:40:00 -04:00
|
|
|
if(slot < 0 || slot >= g_ensPanelUsed || !g_ensPanelSlotUsed[slot])
|
2026-08-15 16:54:43 -04:00
|
|
|
return;
|
|
|
|
|
g_ensPanelLine[slot] = headline;
|
fix(indicators+panel): the dead handle is MEASURED now - recreate it; and order the ensemble panel by member, not by who published first
THE ANSWER, off the instrumentation added in be39674, first run:
ConvLSTM [HYB-2484]: TUNABLE INDICATOR REPORTS NO CALCULATED BARS - 1 tunable
indicator(s) enabled and the least-ready answers BarsCalculated()=-1 ...
Per-indicator depth: price=33982 MA=-1 ZigZag=33982 ATR=33982
MA=-1 with price, ZigZag and ATR all at full depth. **The handle is INVALID, not
short.** Same line on USDJPY (price=50179 MA=-1). Depth was never the problem;
the previous session's five theories were all answering the wrong question.
And it is per-member, not per-chart: LSTM-2484 ran the 34-candidate auto-tune on
that same chart at 15:24:18 and went on to train normally (feature health, 51
features, excursion head) reading the same indicator. Only ConvLSTM's handle -
the last member constructed - was dead. WHY is still not established. All four
members request ADMovingAverage with identical params, so MT5 hands them the SAME
refcounted handle, and the tuner's inner loop is Create-then-IndicatorRelease over
exactly that shared handle; that is the obvious suspect and it is NOT yet proven,
so this commit does not act on it.
1. IndicatorDepthReport() NOW PRINTS HANDLE NUMBERS, not just depths.
"MA=-1" says the handle is dead. "MA=-1(h12)" against another member's "MA=33982
(h12)" says it is the SAME handle and someone released it; "(h-1)" says it was
never created. That is the difference between a refcount bug and a creation
failure and it is one field. This is the measurement the shared-handle suspicion
needs before anyone acts on it.
2. RECREATE A DEAD HANDLE INSTEAD OF SWEEPING AGAINST IT.
A member that cannot read its own indicator must rebuild it. RepairDeadIndicatorHandles()
re-Creates only the ENABLED tunables reporting BarsCalculated() < 0 - a merely COLD
indicator (valid handle, 0 bars) is left alone to warm up the normal way. It does
NOT release first: -1 means the terminal no longer knows the handle, so there is
nothing to give back, and MT5 recycles handle VALUES so releasing a stale one could
decrement whatever now owns that number. 30s cooldown, because every ServableBars()
consumer reaches it including live inference on every tick. The feature cache is
dropped with it, and the log names before/after depths.
Cause-agnostic on purpose. Whatever is killing the handle, sweeping 50,163 bars
against a buffer that answers EMPTY_VALUE at every index - then discarding the era
and doing it again - is not a recovery.
3. THE SWEEP NOW HOLDS ON A DEAD HANDLE.
ServableBars() keeps answering `want` (its contract; live inference and online
learning have their own refusal paths and a 0 there reads as "no history at all").
SettledBars() - the training sweep's entry, the one caller that can afford to wait -
returns 0 instead, so Train() holds and reports rather than burning a full-history
pass it is guaranteed to throw away. A recreated handle is cold, so it primes
through the existing settle path on the next call. If the repair fails the member
holds indefinitely and says so every minute, and be39674's barrier liveness escape
releases the rest of the ensemble after 12 minutes - which is the correct
degradation and is exactly what the log shows happening.
4. THE PANEL ROWS WERE ORDERED BY WHO PUBLISHED FIRST.
Reported on XAUUSD: LSTM, ConvLSTM, Perceptron, Convolutional instead of
Perceptron, Convolutional, LSTM, ConvLSTM. ClaimEnsemblePanelSlot() handed out the
next free row on each member's FIRST PublishStatus() call, so the order was a race -
the members busy sweeping published before the ones sitting idle at the era barrier,
and be39674 sharpened it by (correctly) making a held member stop writing the terse
line. Rows are now keyed to m_ensembleIndex, the registration/construction order,
which is fixed for the life of the chart. Claimed on every publish rather than once,
so it is idempotent and refreshes the tag for a member whose ID was not final when it
first published (the config-tag suffix is appended during InitIndicators, after
EnsembleMember() registers). Unclaimed rows are skipped by the render and excluded
from the model count, so a member that has not published yet leaves no gap and shifts
nobody.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 15:40:00 -04:00
|
|
|
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";
|
2026-08-15 16:54:43 -04:00
|
|
|
for(int i = 0; i < g_ensPanelUsed; i++)
|
fix(indicators+panel): the dead handle is MEASURED now - recreate it; and order the ensemble panel by member, not by who published first
THE ANSWER, off the instrumentation added in be39674, first run:
ConvLSTM [HYB-2484]: TUNABLE INDICATOR REPORTS NO CALCULATED BARS - 1 tunable
indicator(s) enabled and the least-ready answers BarsCalculated()=-1 ...
Per-indicator depth: price=33982 MA=-1 ZigZag=33982 ATR=33982
MA=-1 with price, ZigZag and ATR all at full depth. **The handle is INVALID, not
short.** Same line on USDJPY (price=50179 MA=-1). Depth was never the problem;
the previous session's five theories were all answering the wrong question.
And it is per-member, not per-chart: LSTM-2484 ran the 34-candidate auto-tune on
that same chart at 15:24:18 and went on to train normally (feature health, 51
features, excursion head) reading the same indicator. Only ConvLSTM's handle -
the last member constructed - was dead. WHY is still not established. All four
members request ADMovingAverage with identical params, so MT5 hands them the SAME
refcounted handle, and the tuner's inner loop is Create-then-IndicatorRelease over
exactly that shared handle; that is the obvious suspect and it is NOT yet proven,
so this commit does not act on it.
1. IndicatorDepthReport() NOW PRINTS HANDLE NUMBERS, not just depths.
"MA=-1" says the handle is dead. "MA=-1(h12)" against another member's "MA=33982
(h12)" says it is the SAME handle and someone released it; "(h-1)" says it was
never created. That is the difference between a refcount bug and a creation
failure and it is one field. This is the measurement the shared-handle suspicion
needs before anyone acts on it.
2. RECREATE A DEAD HANDLE INSTEAD OF SWEEPING AGAINST IT.
A member that cannot read its own indicator must rebuild it. RepairDeadIndicatorHandles()
re-Creates only the ENABLED tunables reporting BarsCalculated() < 0 - a merely COLD
indicator (valid handle, 0 bars) is left alone to warm up the normal way. It does
NOT release first: -1 means the terminal no longer knows the handle, so there is
nothing to give back, and MT5 recycles handle VALUES so releasing a stale one could
decrement whatever now owns that number. 30s cooldown, because every ServableBars()
consumer reaches it including live inference on every tick. The feature cache is
dropped with it, and the log names before/after depths.
Cause-agnostic on purpose. Whatever is killing the handle, sweeping 50,163 bars
against a buffer that answers EMPTY_VALUE at every index - then discarding the era
and doing it again - is not a recovery.
3. THE SWEEP NOW HOLDS ON A DEAD HANDLE.
ServableBars() keeps answering `want` (its contract; live inference and online
learning have their own refusal paths and a 0 there reads as "no history at all").
SettledBars() - the training sweep's entry, the one caller that can afford to wait -
returns 0 instead, so Train() holds and reports rather than burning a full-history
pass it is guaranteed to throw away. A recreated handle is cold, so it primes
through the existing settle path on the next call. If the repair fails the member
holds indefinitely and says so every minute, and be39674's barrier liveness escape
releases the rest of the ensemble after 12 minutes - which is the correct
degradation and is exactly what the log shows happening.
4. THE PANEL ROWS WERE ORDERED BY WHO PUBLISHED FIRST.
Reported on XAUUSD: LSTM, ConvLSTM, Perceptron, Convolutional instead of
Perceptron, Convolutional, LSTM, ConvLSTM. ClaimEnsemblePanelSlot() handed out the
next free row on each member's FIRST PublishStatus() call, so the order was a race -
the members busy sweeping published before the ones sitting idle at the era barrier,
and be39674 sharpened it by (correctly) making a held member stop writing the terse
line. Rows are now keyed to m_ensembleIndex, the registration/construction order,
which is fixed for the life of the chart. Claimed on every publish rather than once,
so it is idempotent and refreshes the tag for a member whose ID was not final when it
first published (the config-tag suffix is appended during InitIndicators, after
EnsembleMember() registers). Unclaimed rows are skipped by the render and excluded
from the model count, so a member that has not published yet leaves no gap and shifts
nobody.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 15:40:00 -04:00
|
|
|
if(g_ensPanelSlotUsed[i])
|
|
|
|
|
s += "\n" + g_ensPanelTag[i] + ": " + (g_ensPanelLine[i] == "" ? "starting..." : g_ensPanelLine[i]);
|
fix(ensemble): responsive panel + synchronized eras + combined-vote accuracy
Four user-reported/requested items, one root cause chain:
1) DEAD CONTROL PANEL in AI_HYBRID mode. All members posted custom event
id 1 and handled id 1001, and CExpertCustom broadcasts every chart
event to every filter - so each posted event ran a train chunk in ALL
N members (N*N chunks per round) and the chart thread never idled
long enough to deliver clicks/drags. profiling.csv: 99.45% of time in
OnChartEventHandler. Fix: per-instance study-event ids
(STUDY_EVENT_ID_BASE + construction order, offset above the Controls
library's ON_* codes - id 1 was also ON_DBL_CLICK, so panel
double-clicks fired training chunks). ArmStudyEvent() is the single
post site; lost-event watchdog replaces the accidental
sibling-clears-my-flag rescue.
2) WARM-UP DUPLICATION. The auto-tune sweep is deterministic over
identical features/labels, and it ends in the full MI diagnostic
suite, which the MI-share gate never intercepted on the sweep path -
four members ran four identical ~36s sweep+report blocks. First
member publishes outcome (g_ensembleChartTuneDone/Installed/Settings);
the rest apply it and skip both.
3) DEINIT STRANDED PANEL+ARROWS (user repro 18:52). Root cause from the
log: the 4,500ms budget runs from MetaTrader's stop REQUEST - a heavy
autosave in flight ate it, OnDeinit got ~430ms and died in the first
member's arrow persist ("Abnormal termination" 432ms in). Fix: early
visible-UI sweep (native prefix deletes for status/panel/dialog)
right after ClearStatusLabel, and a fast path for still-training
models - their arrows are re-rendered every era, so they get one bulk
purge instead of scan+atomic-write in the death window.
4) ENSEMBLE FEATURES (user requests): era BARRIER - members advance era
by era together; a member ahead of the slowest still-training member
declines Train() calls and its chunk budget is donated
(TRAIN_TIME_BUDGET_MS = 120/activeTrainers, UI headroom constant).
COMBINED-VOTE OOS SCORE - each member's pass-3 scan contributes its
adjusted per-bar decision (0.0 on abstain) to a shared row buffer;
the last member to finish the era scores the averaged vote vs the
mirrored Min_Vote_Open against the same target-before-stop outcomes
members grade themselves on, publishing an "Ensemble vote" line on
the aggregated panel. Member headlines now carry their lifetime win
rate with break-even.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:06:04 -04:00
|
|
|
if(g_ensembleVoteLine != "")
|
|
|
|
|
s += "\n" + g_ensembleVoteLine;
|
2026-08-15 16:54:43 -04:00
|
|
|
//--- 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);
|
|
|
|
|
}
|