Warrior_EA/System/StatusLabel.mqh

265 lines
12 KiB
MQL5
Raw Permalink Normal View History

//+------------------------------------------------------------------+
//| 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.
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
bool g_ensPanelSlotUsed[ENSEMBLE_PANEL_MAX_MEMBERS];
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.
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
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)
{
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;
}
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])
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";
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;
//--- 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);
}