Warrior_EA/Expert/Chart/ChartUI.mqh
AnimateDread b92e233b88 fix(chart): a deployed model rescans history to rebuild its vote arrows
The sidecar added in 484a9d8 restores the vote arrows from the previous
session - but there was no previous session to restore from, and a
deployed ensemble could never produce one.

The overlay that draws the vote layer replays each member's
m_overlaySigSnap, published in exactly one place: RankTiersFromOos, at
pass-3 completion. A converged model runs no further eras. So after a
restart every member's snapshot was empty, would never fill, the sweep
had nothing to replay and the chart stayed blank permanently - no route
back by any path.

The chart rescan is the route: it runs the DEPLOYED net forward over
history and rebuilds the per-bar cache, which is the same quantity pass 3
produces, obtained without training. It already existed for the panel's
Show-Signals button; it just never handed its result to the overlay, so
on the default filtered view a rescan rebuilt only the RAW per-member
layer - the one that is hidden - and appeared to do nothing.

- PublishOverlaySnapshotFromCache() extracted from RankTiersFromOos, so
  the era end and a completed rescan publish through one implementation.
- A completed rescan now calls it, which also arms the sweep.
- PollTraining auto-arms one rescan for a model that is converged, has no
  snapshot, and is on the filtered view. One-shot: a model that
  legitimately calls Neutral everywhere must not rescan forever chasing a
  snapshot that is correctly empty. On the timer, not in OnInit - it is a
  full feedForward per bar over up to 5000 bars and drains in the same
  time-boxed slices as a manual rescan.

Together with the sidecar this closes both halves: the rescan covers the
first session and any chart whose file was lost or invalidated by a
threshold change; the sidecar covers every session after one is saved.

Compile-verified in _claude_stage: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 13:11:36 -04:00

1006 lines
54 KiB
MQL5

//+------------------------------------------------------------------+
//| Warrior_EA |
//| AnimateDread |
//| |
//| CChartUI - chart arrows, arrow persistence/restore, the status |
//| panel, the HUD line and chart cleanup for ONE model. |
//+------------------------------------------------------------------+
#ifndef WARRIOR_CHART_CHARTUI_MQH
#define WARRIOR_CHART_CHARTUI_MQH
//+------------------------------------------------------------------+
//| Everything this owns is chart-rendering state nothing else in the |
//| signal touches: the arrow-restore queue, the rescan queue/tally, |
//| the last-arrows-saved count, the purge-mismatch latch, and the |
//| status-label throttle/cache. Everything it READS - training |
//| counters, vote/meta state, bar series, the deployed net - comes |
//| through a CChartView, never `this` on the signal (MQL5 has no |
//| friend, so that view is the only door - see |
//| Expert\Chart\IChartView.mqh and Expert\project_oop_module_pattern).|
//+------------------------------------------------------------------+
class CChartUI
{
private:
//--- BORROWED. The signal owns both the view and this object.
CChartView *m_view;
//--- parsed-but-not-yet-drawn arrows, consumed by AdvanceChartSignalRestore.
datetime m_arrowRestoreTime[];
int m_arrowRestoreCode[];
double m_arrowRestorePrice[];
long m_arrowRestoreTf[];
int m_arrowRestoreIndex;
bool m_arrowRestorePending;
uint m_arrowRestoreStartMs;
//--- the per-bar rescan loop's cursor/bound/tally, drained by AdvanceChartSignalRescan.
int m_rescanIndex;
int m_rescanHi;
int m_rescanBarsNow;
bool m_rescanPending;
uint m_rescanStartMs;
//--- Raw (PRE prior-correction) argmax tally, accumulated per-bar across AdvanceChartSignalRescan's
//--- slices - lets the completion log distinguish "the network itself calls Neutral almost
//--- everywhere" from "the network still discriminates, but the logit-prior correction is
//--- suppressing it down to Neutral" - both produce an identical all-Neutral cache/empty chart.
int m_rescanRawBuy;
int m_rescanRawSell;
int m_rescanRawNeutral;
//--- How many arrows the last successful SaveChartSignals() wrote - reporting only.
int m_lastArrowsSaved;
//--- One-shot latch for PurgeChart()'s "saved N but the chart holds none" warning. PurgeChart
//--- runs twice on a clean removal - once from the shutdown path and again from the destructor,
//--- which is deliberate - and the second call necessarily finds an already-emptied chart with
//--- m_lastArrowsSaved still set.
bool m_purgeMismatchWarned;
//--- Per-instance wall-clock throttle gate for UpdateTrainingStatusLabel()'s ChartRedraw().
uint m_lastStatusLabelUpdateTick;
//--- Last values passed to UpdateTrainingStatusLabel() - cached (updated on EVERY call, throttled
//--- or not) so the forced era-end refresh has something real to redraw with instead of a
//--- stale/zeroed placeholder, since no "current bar" exists once an era's own three passes are done.
double m_lastDisplayNeuron0, m_lastDisplayNeuron1, m_lastDisplayNeuron2, m_lastDisplaySignal;
public:
CChartUI(void) :
m_view(NULL),
m_arrowRestoreIndex(0),
m_arrowRestorePending(false),
m_arrowRestoreStartMs(0),
m_rescanIndex(0),
m_rescanHi(0),
m_rescanBarsNow(0),
m_rescanPending(false),
m_rescanStartMs(0),
m_rescanRawBuy(0),
m_rescanRawSell(0),
m_rescanRawNeutral(0),
m_lastArrowsSaved(0),
m_purgeMismatchWarned(false),
m_lastStatusLabelUpdateTick(0),
m_lastDisplayNeuron0(0),
m_lastDisplayNeuron1(0),
m_lastDisplayNeuron2(0),
m_lastDisplaySignal(0)
{ }
~CChartUI(void) { m_view = NULL; }
//--- Hand it the view once, when the owner is constructed. Everything else it needs it asks for.
void Bind(CChartView *view) { m_view = view; }
bool RescanPending(void) const { return m_rescanPending; }
bool ArrowRestorePending(void) const { return m_arrowRestorePending; }
void ClearPersistedChartSignals(const string reason);
bool SaveChartSignals(bool pruneChartObjects = true);
void PersistAndClearChartSignals(void);
void LoadChartSignals(void);
void AdvanceChartSignalRestore(void);
bool StartChartSignalRescan(void);
void AdvanceChartSignalRescan(void);
string ComputeCompoundedAccuracyLine(void);
void UpdateTrainingStatusLabel(const string &progressLine, double neuron0, double neuron1,
double neuron2, double signalValue, bool forceRefresh = false);
//--- Forced (unthrottled) panel refresh from the LAST cached values UpdateTrainingStatusLabel saw -
//--- for the era-end console line's matching panel refresh (Training.mqh), which has no "current
//--- bar" of its own to pass in once an era's three passes are done.
void RefreshStatusLabel(void)
{ UpdateTrainingStatusLabel("Era complete", m_lastDisplayNeuron0, m_lastDisplayNeuron1, m_lastDisplayNeuron2, m_lastDisplaySignal, true); }
void DrawObject(datetime time, double signal, double close);
void DeleteObject(datetime time);
void PruneDirectionalClusters(int bars);
int PurgeChart(void);
};
//+------------------------------------------------------------------+
//| Erase this model's drawn arrows, its .arrows sidecar, and any |
//| deferred restore still in flight. |
//+------------------------------------------------------------------+
void CChartUI::ClearPersistedChartSignals(const string reason)
{
if(MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_FORWARD))
return;
//--- Cancel any deferred restore FIRST. Otherwise its already-parsed queue keeps drawing the old
//--- model's arrows from the next timer slice onward - after the file was deleted - and the next
//--- save writes them straight back out. See AdvanceChartSignalRestore.
m_arrowRestorePending = false;
m_arrowRestoreIndex = 0;
ArrayFree(m_arrowRestoreTime);
ArrayFree(m_arrowRestoreCode);
ArrayFree(m_arrowRestorePrice);
ArrayFree(m_arrowRestoreTf);
string arrows = m_view.FileName() + ".arrows";
ResetLastError();
if(FileIsExist(arrows, FILE_COMMON) && !FileDelete(arrows, FILE_COMMON))
Print(m_view.Id() + ": ERROR - failed to delete " + arrows + ", error " + IntegerToString(GetLastError()));
ResetLastError();
//--- Clear them off the chart too, so the reset is visibly complete instead of leaving stale arrows
//--- on screen until the next restart. Namespaced delete, THIS member's arrows only - other
//--- ensemble members' arrows and user drawings are untouched.
ObjectsDeleteAll(0, m_view.ArrowPrefix());
ChartRedraw(0);
PrintVerbose(m_view.Id() + ": cleared drawn signal arrows and " + arrows + " - " + reason);
}
//+------------------------------------------------------------------+
//| Persist the currently-drawn directional arrows to a sidecar file |
//| so they survive an EA remove/re-add/recompile without a retrain. |
//| Captures time, arrow code (217 Buy / 218 Sell), price, and hide |
//| state; chart-only (no persistent chart in a backtest). |
//+------------------------------------------------------------------+
bool CChartUI::SaveChartSignals(bool pruneChartObjects)
{
if(MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_FORWARD))
return true;
long times[];
int codes[];
double prices[];
long tfs[];
int n = 0;
int total = ObjectsTotal(0);
string arrowPrefix = m_view.ArrowPrefix();
//--- Pre-size the arrays ONCE to an upper bound (all chart objects), then fill and trim. This
//--- keeps the whole scan O(n). room for the chart's own arrows PLUS any still sitting in the
//--- deferred-restore queue (see below)
int queueRemaining = m_arrowRestorePending ? (ArraySize(m_arrowRestoreTime) - m_arrowRestoreIndex) : 0;
if(queueRemaining < 0)
queueRemaining = 0;
ArrayResize(times, total + queueRemaining);
ArrayResize(codes, total + queueRemaining);
ArrayResize(prices, total + queueRemaining);
ArrayResize(tfs, total + queueRemaining);
for(int i = 0; i < total; i++)
{
string nm = ObjectName(0, i);
//--- member-scoped scan: on an ensemble chart the bare prefix would sweep every
//--- OTHER member's arrows into this member's sidecar (cross-model laundering)
if(StringFind(nm, arrowPrefix) != 0)
continue;
//--- OBJ_TREND ONLY, deliberately, even though a mark is a line AND an arrow since
//--- 2026-08-20: the line is the canonical half (it carries the trigger price the sidecar
//--- stores), and counting both would write every mark twice.
if(ObjectGetInteger(0, nm, OBJPROP_TYPE) != OBJ_TREND)
continue;
times[n] = (long)ObjectGetInteger(0, nm, OBJPROP_TIME, 0);
//--- A line carries no arrow code, so DIRECTION is recovered from the colour - the one
//--- property that now means direction and nothing else. Persisted as the old Wingdings
//--- numbers purely so existing sidecar files keep loading (see WARRIOR_SIG_CODE_BUY).
codes[n] = ((color)ObjectGetInteger(0, nm, OBJPROP_COLOR) == WARRIOR_SIG_BUY_COLOR)
? WARRIOR_SIG_CODE_BUY : WARRIOR_SIG_CODE_SELL;
prices[n] = ObjectGetDouble(0, nm, OBJPROP_PRICE, 0);
tfs[n] = g_signalsVisible ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS;
n++;
}
//--- CRITICAL for init/deinit sync: a deferred restore may still be in flight (see
//--- AdvanceChartSignalRestore), meaning part of the saved history has NOT been drawn yet and so
//--- is invisible to the chart scan above.
for(int q = m_arrowRestoreIndex; q < m_arrowRestoreIndex + queueRemaining; q++)
{
times[n] = (long)m_arrowRestoreTime[q];
codes[n] = m_arrowRestoreCode[q];
prices[n] = m_arrowRestorePrice[q];
tfs[n] = g_signalsVisible ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS;
n++;
}
if(n <= 0)
return true; // nothing to save - leave any existing file intact (and nothing on the chart to lose)
//--- Keep only the MAX_PERSISTED_ARROWS most recent, by TIME (see the define's comment for why
//--- scan order can't be used). Compaction is safe in place because the write index never runs
//--- ahead of the read index.
long pruned[];
int prunedCount = 0;
if(n > MAX_PERSISTED_ARROWS)
{
long sortedTimes[];
ArrayResize(sortedTimes, n);
ArrayCopy(sortedTimes, times, 0, 0, n);
ArraySort(sortedTimes); // ascending
long cutoff = sortedTimes[n - MAX_PERSISTED_ARROWS];
ArrayResize(pruned, n);
int w = 0;
for(int i = 0; i < n; i++)
{
//--- ">= cutoff" can match more than the cap when several arrows share the cut-off timestamp;
//--- the w < cap test keeps the kept set at exactly the cap in that case.
if(times[i] >= cutoff && w < MAX_PERSISTED_ARROWS)
{
times[w] = times[i];
codes[w] = codes[i];
prices[w] = prices[i];
tfs[w] = tfs[i];
w++;
}
else
pruned[prunedCount++] = times[i];
}
n = w;
}
//--- Staged through a temp file + atomic rename (System\AtomicFile.mqh), so an interrupted write
//--- keeps the previous arrow set instead of truncating it, and never blocks LoadChartSignals()
//--- on another instance.
string arrowsName = m_view.FileName() + ".arrows";
string arrowsTmpName = "";
int handle = AtomicWriteBegin(arrowsName, FILE_COMMON, arrowsTmpName);
if(handle == INVALID_HANDLE)
{
//--- Was a silent `return`. Name the file and the error so the next occurrence is one grep.
Print(m_view.Id() + ": ERROR - could not open " + arrowsTmpName + " to persist " + IntegerToString(n) +
" chart signal arrows, error " + IntegerToString(GetLastError()) +
" - the arrows stay on the chart (they are the only copy).");
return false;
}
//--- Results ARE checked now: the pruning step below is documented as running only after a
//--- successful write, but nothing used to verify that, so a failed/partial write still deleted
//--- the chart objects and lost those arrows in both places.
bool ok = (FileWriteInteger(handle, 0x57534152) > 0); // 'WSAR' magic
if(ok && FileWriteInteger(handle, n) <= 0)
ok = false;
for(int i = 0; ok && i < n; i++)
{
if(FileWriteLong(handle, times[i]) <= 0 || FileWriteInteger(handle, codes[i]) <= 0 ||
FileWriteDouble(handle, prices[i]) <= 0 || FileWriteLong(handle, tfs[i]) <= 0)
ok = false;
}
if(!AtomicWriteEnd(handle, arrowsName, arrowsTmpName, FILE_COMMON, ok, __FUNCTION__))
return false; // AtomicWriteEnd already logged which half failed
//--- Only after the file is safely written: drop the pruned arrows from the chart too, so the
//--- chart and the sidecar stay in agreement and the object count stops growing without bound.
if(prunedCount > 0 && pruneChartObjects)
{
for(int i = 0; i < prunedCount; i++)
WarriorDeleteSignalMark(arrowPrefix + TimeToString((datetime)pruned[i]));
ChartRedraw(0);
PrintVerbose(m_view.Id() + ": pruned " + IntegerToString(prunedCount) + " old chart signal arrows (keeping the " +
IntegerToString(MAX_PERSISTED_ARROWS) + " most recent)");
}
m_lastArrowsSaved = n;
return true;
}
//+------------------------------------------------------------------+
//| Shutdown path: hand the drawn arrows over to the save/restore |
//| mechanism and take them OFF the chart, rather than abandoning |
//| them there for the next EA (or the user) to find. |
//+------------------------------------------------------------------+
void CChartUI::PersistAndClearChartSignals(void)
{
if(MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_FORWARD))
{
PurgeChart(); // tester charts are throwaway - nothing to persist, just leave nothing behind
return;
}
m_lastArrowsSaved = 0;
//--- FAST PATH FOR A STILL-TRAINING MODEL, 2026-08-16. The write-then-clear doctrine below
//--- exists because a CONVERGED model's chart arrows are the only copy of its history (nothing
//--- redraws them).
if(!m_view.TrainingComplete())
{
int removedTraining = PurgeChart();
Print(m_view.Id() + ": chart signals - model still training, cleared " + IntegerToString(removedTraining) +
" arrow(s) without a sidecar rewrite (a training model re-renders them every era; the "
"existing sidecar is kept for the next attach).");
return;
}
//--- pruneChartObjects=false: the purge below removes every arrow in one bulk call, so deleting the
//--- over-cap ones individually first is pure cost - and at O(objects) per ObjectDelete it is the cost
//--- that overran MT5's deinit budget on 2026-08-01 and skipped the cleanup entirely.
bool saved = SaveChartSignals(false);
if(!saved)
{
Print(m_view.Id() + ": WARNING - could not persist the drawn signal arrows, so they are being LEFT on the chart "
"rather than discarded (they are the only copy). Clear them with the panel's reset-weights, or "
"fix the write error logged above.");
ClearStatusLabel();
return;
}
int removed = PurgeChart();
Print(m_view.Id() + ": chart signals - persisted " + IntegerToString(m_lastArrowsSaved) + " arrow(s) to " +
m_view.FileName() + ".arrows and cleared " + IntegerToString(removed) +
" from the chart; they are restored automatically the next time this configuration is attached.");
}
//+------------------------------------------------------------------+
//| Recreate the arrows saved by SaveChartSignals(). Runs at init so |
//| a re-added / recompiled / restarted chart shows the deployed |
//| model's signals again without retraining. Restores the saved hide |
//| state, so the show/hide toggle survives too. |
//+------------------------------------------------------------------+
void CChartUI::LoadChartSignals(void)
{
if(MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_FORWARD))
return;
//--- ARROWS BELONG TO A MODEL. ORPHAN SWEEP, UNCONDITIONAL AND FIRST. MEMBER-scoped: on an ensemble
//--- chart four members init in sequence, and a bare-prefix sweep here would wipe the arrows the
//--- previous member just restored.
ObjectsDeleteAll(0, m_view.ArrowPrefix());
if(!m_view.ModelLoadedFromDisk())
{
ClearPersistedChartSignals("no saved model for this configuration - starting with a clean chart");
return;
}
string fn = m_view.FileName() + ".arrows";
if(!FileIsExist(fn, FILE_COMMON))
{
ChartRedraw(0);
return;
}
//--- share flags: read-only, see CopySharedFile().
int handle = FileOpen(fn, FILE_COMMON | FILE_BIN | FILE_READ | FILE_SHARE_READ | FILE_SHARE_WRITE);
if(handle == INVALID_HANDLE)
return;
if(FileReadInteger(handle) != 0x57534152)
{
FileClose(handle);
return;
}
int n = FileReadInteger(handle);
//--- Sanity-cap a corrupt/garbage count so a bad header can never queue millions of object creations.
if(n < 0 || n > MAX_RESTORED_ARROWS)
{
Print(m_view.Id() + ": .arrows header declares " + IntegerToString(n) + " arrows - refusing to restore (limit " +
IntegerToString(MAX_RESTORED_ARROWS) + "); the file looks corrupt. Chart signals will rebuild as the model runs.");
FileClose(handle);
return;
}
ArrayResize(m_arrowRestoreTime, n);
ArrayResize(m_arrowRestoreCode, n);
ArrayResize(m_arrowRestorePrice, n);
ArrayResize(m_arrowRestoreTf, n);
int parsed = 0;
for(int i = 0; i < n && !FileIsEnding(handle); i++)
{
m_arrowRestoreTime[parsed] = (datetime)FileReadLong(handle);
m_arrowRestoreCode[parsed] = FileReadInteger(handle);
m_arrowRestorePrice[parsed] = FileReadDouble(handle);
m_arrowRestoreTf[parsed] = FileReadLong(handle);
parsed++;
}
FileClose(handle);
//--- Trim to what actually parsed (a truncated file yields fewer records than the header claimed).
if(parsed != n)
{
ArrayResize(m_arrowRestoreTime, parsed);
ArrayResize(m_arrowRestoreCode, parsed);
ArrayResize(m_arrowRestorePrice, parsed);
ArrayResize(m_arrowRestoreTf, parsed);
}
//--- Apply the retention cap on the way IN as well, so a pre-cap file (one run had 2896 arrows)
//--- is trimmed on its first load instead of waiting for the next save - otherwise the very
//--- restore this cap exists to bound would still rebuild every one of them.
if(parsed > MAX_PERSISTED_ARROWS)
{
long sortedTimes[];
ArrayResize(sortedTimes, parsed);
for(int i = 0; i < parsed; i++)
sortedTimes[i] = (long)m_arrowRestoreTime[i];
ArraySort(sortedTimes); // ascending
long cutoff = sortedTimes[parsed - MAX_PERSISTED_ARROWS];
int w = 0;
for(int i = 0; i < parsed; i++)
if((long)m_arrowRestoreTime[i] >= cutoff && w < MAX_PERSISTED_ARROWS)
{
m_arrowRestoreTime[w] = m_arrowRestoreTime[i];
m_arrowRestoreCode[w] = m_arrowRestoreCode[i];
m_arrowRestorePrice[w] = m_arrowRestorePrice[i];
m_arrowRestoreTf[w] = m_arrowRestoreTf[i];
w++;
}
Print(m_view.Id() + ": .arrows sidecar holds " + IntegerToString(parsed) + " arrows - restoring the " +
IntegerToString(w) + " most recent (retention cap " + IntegerToString(MAX_PERSISTED_ARROWS) +
"); the file is rewritten capped on the next save.");
parsed = w;
ArrayResize(m_arrowRestoreTime, parsed);
ArrayResize(m_arrowRestoreCode, parsed);
ArrayResize(m_arrowRestorePrice, parsed);
ArrayResize(m_arrowRestoreTf, parsed);
}
m_arrowRestoreIndex = 0;
m_arrowRestorePending = (parsed > 0);
m_arrowRestoreStartMs = GetTickCount();
//--- Deliberately does NOT draw anything here - see AdvanceChartSignalRestore's declaration comment for
//--- why this must not block OnInit. Drawing starts on the next timer/tick slice.
if(m_arrowRestorePending)
PrintVerbose(m_view.Id() + ": queued " + IntegerToString(parsed) + " chart signal arrows for progressive restore");
}
//+------------------------------------------------------------------+
//| See the declaration comment - draws a time-boxed slice of the |
//| arrows queued by LoadChartSignals(), so a chart carrying |
//| thousands of them fills in progressively instead of freezing the |
//| terminal during init. |
//+------------------------------------------------------------------+
void CChartUI::AdvanceChartSignalRestore(void)
{
if(!m_arrowRestorePending)
return;
int total = ArraySize(m_arrowRestoreTime);
uint sliceStart = GetTickCount();
int drawnThisSlice = 0;
while(m_arrowRestoreIndex < total)
{
//--- check the budget every 64 objects rather than every one: GetTickCount() itself is not free, and
//--- at ~microseconds per object a per-iteration clock read would dominate the work being measured.
if(drawnThisSlice > 0 && (drawnThisSlice & 63) == 0 && GetTickCount() - sliceStart >= ARROW_RESTORE_BUDGET_MS)
break;
//--- ...and stop outright if the program is unloading. The slice budget bounds THROUGHPUT,
//--- not latency to a stop, and this queue can hold up to MAX_RESTORED_ARROWS entries - the
//--- longer the chart history, the more there are.
if(drawnThisSlice > 0 && (drawnThisSlice & 63) == 0 && m_view.Stopping())
return;
datetime t = m_arrowRestoreTime[m_arrowRestoreIndex];
int code = m_arrowRestoreCode[m_arrowRestoreIndex];
double price = m_arrowRestorePrice[m_arrowRestoreIndex];
long tf = m_arrowRestoreTf[m_arrowRestoreIndex];
m_arrowRestoreIndex++;
drawnThisSlice++;
string nm = m_view.ArrowPrefix() + TimeToString(t);
//--- Deliberately NO ObjectFind() pre-check. ObjectCreate already returns false when the name
//--- exists, and re-applying the properties below is harmless and exactly what a refresh
//--- does.
WarriorPlotSignalLevel(nm, t, m_view.Period(), price, code == WARRIOR_SIG_CODE_BUY,
false, m_view.DisplayNameForChart() + " (restored)");
//--- Both halves, or a restore made while signals are hidden brings the arrows back alone.
ObjectSetInteger(0, nm, OBJPROP_TIMEFRAMES, tf); // restore the saved show/hide state
ObjectSetInteger(0, WarriorSignalArrowName(nm), OBJPROP_TIMEFRAMES, tf);
}
ChartRedraw(0);
if(m_arrowRestoreIndex < total)
return; // more slices to come
//--- done - release the buffers and report once
m_arrowRestorePending = false;
Print(m_view.Id() + ": restored " + IntegerToString(total) + " chart signal arrows in " +
IntegerToString((int)(GetTickCount() - m_arrowRestoreStartMs)) + " ms (progressive, non-blocking)");
ArrayFree(m_arrowRestoreTime);
ArrayFree(m_arrowRestoreCode);
ArrayFree(m_arrowRestorePrice);
ArrayFree(m_arrowRestoreTf);
}
//+------------------------------------------------------------------+
//| Manual "rescan" of the drawn signal arrows: purges every arrow |
//| currently on the chart (namespaced delete - user drawings |
//| untouched) and re-infers the last SIGNAL_RESCAN_LOOKBACK_BARS |
//| bars from the CURRENTLY deployed weights, then re-runs the same |
//| end-of-era NMS declutter (PruneDirectionalClusters) used during |
//| training so the fresh set matches what a live re-render would |
//| have produced. Wired to the panel's Hide->Show Signals sequence: |
//| without this, "restore" only ever replays whatever was last saved |
//| to the .arrows sidecar, which for a long-deployed model can be a |
//| stale historical render from whenever it was last actually |
//| trained - years-old arrows crowding out anything recent. |
//| Chart-only (no persistent chart in the tester/optimizer) and a |
//| no-op until a model has something to infer with. |
//| |
//| This only does the cheap setup (buffer resize, arrow purge, cache |
//| alloc) and QUEUES the per-bar inference loop for |
//| AdvanceChartSignalRescan() to drain in time-boxed slices off the |
//| timer - see that method's comment for why the loop itself must |
//| never run in one blocking pass. Returns true once a rescan has |
//| been queued (check RescanPending() for completion), false if |
//| there was nothing to rescan (no deployed model, tester/optimizer |
//| context, etc). |
//+------------------------------------------------------------------+
bool CChartUI::StartChartSignalRescan(void)
{
if(MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_FORWARD))
return false;
if(!m_view.NetReady() || !m_view.TrainingComplete())
return false;
int outputNeurons = m_view.OutputNeuronsCount();
if(outputNeurons != 1 && outputNeurons != 3)
return false;
int barsAvail = m_view.AvailableBars();
int barsNow = MathMin(SIGNAL_RESCAN_LOOKBACK_BARS, barsAvail);
//--- SIGNAL_RESCAN_LOOKBACK_BARS is 5000 and the SMALLEST "Max bars in chart" setting MT5 offers
//--- is also 5000, so this call site genuinely can be capped - it would redraw the whole rescan
//--- window as Neutral (every feature window rejected) and read as "the model calls nothing".
barsNow = m_view.ServableBars(barsNow, "chart rescan");
if(barsNow <= m_view.HistoryBars())
return false;
if(!m_view.ResizeBuffers(barsNow) || !m_view.RefreshData())
return false;
m_view.EnsureShadowNet();
//--- Drop only the arrows THIS rescan is about to re-judge - i.e. those within [now, oldest
//--- bar of the barsNow window] - not every namespaced arrow on the chart. This scoped delete
//--- is the fix - older arrows are never in scope to be wiped in the first place.
datetime rescanCutoffTime = m_view.BarTime(barsNow - 1);
//--- TYPED-BLIND, PREFIX-SCOPED. A mark is a line AND an arrow since 2026-08-20, and a typed
//--- scan clears only the half it names - leaving stale arrows on bars the rescan no longer
//--- signals. The prefix test is what keeps a typed-blind sweep off the user's own drawings.
//--- Deliberately the BARE SIG_ARROW_PREFIX, not this model's own ArrowPrefix() - same as the
//--- code this replaces.
for(int oi = ObjectsTotal(0, -1, -1) - 1; oi >= 0; oi--)
{
string onm = ObjectName(0, oi, -1, -1);
if(StringFind(onm, SIG_ARROW_PREFIX) != 0)
continue;
if((datetime)ObjectGetInteger(0, onm, OBJPROP_TIME) >= rescanCutoffTime)
ObjectDelete(0, onm);
}
m_view.ResizePredictionCache(barsNow, -2.0);
m_rescanBarsNow = barsNow;
m_rescanHi = barsNow - m_view.HistoryBars();
m_rescanIndex = 0;
m_rescanRawBuy = 0;
m_rescanRawSell = 0;
m_rescanRawNeutral = 0;
m_rescanPending = (m_rescanHi > 0);
m_rescanStartMs = GetTickCount();
if(m_rescanPending)
Print(m_view.Id() + ": rescanning last " + IntegerToString(m_rescanHi) + " bars against the deployed model (progressive, non-blocking)...");
return m_rescanPending;
}
//+------------------------------------------------------------------+
//| Time-boxed slice of the per-bar re-inference queued by |
//| StartChartSignalRescan(). |
//+------------------------------------------------------------------+
void CChartUI::AdvanceChartSignalRescan(void)
{
if(!m_rescanPending)
return;
uint sliceStart = GetTickCount();
int outputNeurons = m_view.OutputNeuronsCount();
while(m_rescanIndex < m_rescanHi)
{
if(GetTickCount() - sliceStart >= ARROW_RESTORE_BUDGET_MS)
break;
//--- Stop request beats the slice budget - see AdvanceChartSignalRestore's matching comment.
//--- This one runs a full feedForward per bar over up to SIGNAL_RESCAN_LOOKBACK_BARS bars, so
//--- it is the most expensive per-iteration loop on the timer path.
if(m_view.Stopping())
return;
int i = m_rescanIndex;
m_rescanIndex++;
double rawSignal = 0.0, adjustedSignal = -2.0;
//--- Same builder the live signal uses, so a restored arrow means what the deployed model would
//--- actually have said on that bar rather than a lookalike assembled by a parallel loop.
if(!m_view.ScoreBarForRescan(i, rawSignal, adjustedSignal))
continue;
if(outputNeurons != 1)
{
switch(m_view.ToSignal(rawSignal))
{
case Buy:
m_rescanRawBuy++;
break;
case Sell:
m_rescanRawSell++;
break;
default:
m_rescanRawNeutral++;
break;
}
}
m_view.SetPredictionAt(i, adjustedSignal);
}
if(m_rescanIndex < m_rescanHi)
return; // more slices to come
//--- done - render the declustered set once, same as the old blocking pass did
PruneDirectionalClusters(m_rescanBarsNow);
SaveChartSignals();
//--- ...and hand the freshly-rebuilt cache to the overlay as this member's snapshot. Without this
//--- the rescan rebuilt only the RAW per-member layer, which is the one hidden while
//--- DrawUnfilteredSignals is off - so on the default view a rescan appeared to do nothing at all.
//--- Publishing here is what lets a DEPLOYED model's vote arrows be rebuilt: it runs no further
//--- eras, and the era end is the only other place a snapshot is ever produced.
m_view.PublishOverlaySnapshot();
m_rescanPending = false;
//--- Tally what the model actually called BEFORE decluttering, so an empty-looking chart is
//--- distinguishable in the log between "the model called Neutral almost everywhere" (a real,
//--- if extreme, calibration outcome) and "arrows were computed but never rendered" (a bug) -
//--- both look identical on the chart otherwise.
int buyCount = 0, sellCount = 0, neutralCount = 0, unscored = 0;
for(int k = 0; k < m_rescanHi; k++)
{
double sv = m_view.PredictionAt(k);
if(sv == -2.0)
{
unscored++;
continue;
}
switch(m_view.ToSignal(sv))
{
case Buy:
buyCount++;
break;
case Sell:
sellCount++;
break;
default:
neutralCount++;
break;
}
}
Print(m_view.Id() + ": rescanned " + IntegerToString(m_rescanBarsNow) + " bars against the deployed model and rebuilt the chart signal arrows in " +
IntegerToString((int)(GetTickCount() - m_rescanStartMs)) + " ms (progressive, non-blocking) - post-calibration calls (pre-decluttering): " +
IntegerToString(buyCount) + " Buy, " + IntegerToString(sellCount) + " Sell, " + IntegerToString(neutralCount) + " Neutral, " +
IntegerToString(unscored) + " unscored" +
(outputNeurons == 3 ? " | RAW network argmax (before logit-prior correction): " +
IntegerToString(m_rescanRawBuy) + " Buy, " + IntegerToString(m_rescanRawSell) + " Sell, " +
IntegerToString(m_rescanRawNeutral) + " Neutral" : ""));
}
//+------------------------------------------------------------------+
//| Compounded/persistent accuracy line for the simple panels. |
//| See the declaration comment (class body) for why this exists. |
//+------------------------------------------------------------------+
string CChartUI::ComputeCompoundedAccuracyLine(void)
{
long cumIsTotal = m_view.CumIsTotal();
long cumOosTotal = m_view.CumOosTotal();
if(cumIsTotal <= 0 && cumOosTotal <= 0)
{
//--- These counters are LIFETIME and persisted (they are what the panel presents as the
//--- product's accuracy), so they only advance on bars the model CALLED. They sit at zero in
//--- two very different situations and the panel must not describe both as a wait.
if(m_view.EraCount() > 0)
return "Buy/Sell calls correct: no directional calls yet";
return "Buy/Sell calls correct: measuring...";
}
//--- OUT-OF-SAMPLE only on the panel. The IS/OOS pair is genuinely useful (the gap between them
//--- is the over-fitting read), so it is not discarded, just moved to the journal under
//--- DebuggingMode where diagnosing it belongs.
if(cumOosTotal > 0)
{
//--- Labelled "lifetime" deliberately. At era 200+ a single new era shifts it by a fraction
//--- of a percent, so a model that started badly and has since recovered still reads low
//--- here.
int winPct = (int)MathRound(m_view.CumOosCorrect() * 100.0 / cumOosTotal);
//--- THIS ERA, alongside the lifetime figure (user report 2026-08-16: the lifetime number
//--- "doesn't seem to change" even though the arrows visibly swing between clearly good and
//--- clearly bad stretches).
int buyPredicted = 0, sellPredicted = 0, buyPredictedHits = 0, sellPredictedHits = 0;
m_view.OosTally(buyPredicted, sellPredicted, buyPredictedHits, sellPredictedHits);
int thisEraTotal = buyPredicted + sellPredicted;
string thisEra = (thisEraTotal > 0)
? ", this era " + IntegerToString((int)MathRound(
(buyPredictedHits + sellPredictedHits) * 100.0 / thisEraTotal)) + "%"
: "";
return "Buy/Sell calls correct: " + IntegerToString(winPct) + "% (unseen data" + thisEra + ")";
}
return "Buy/Sell calls correct: " +
IntegerToString((int)MathRound(m_view.CumIsCorrect() * 100.0 / cumIsTotal)) +
"% (training data so far)";
}
//+------------------------------------------------------------------+
//| See the declaration comment (class body) for why this exists. |
//+------------------------------------------------------------------+
void CChartUI::UpdateTrainingStatusLabel(const string &progressLine, double neuron0, double neuron1, double neuron2, double signalValue, bool forceRefresh)
{
// Cache regardless of whether this particular call actually redraws below - see
// m_lastDisplayNeuron0's declaration comment for why the era-end forced refresh needs these.
m_lastDisplayNeuron0 = neuron0;
m_lastDisplayNeuron1 = neuron1;
m_lastDisplayNeuron2 = neuron2;
m_lastDisplaySignal = signalValue;
// Throttle: SetStatusLabel()'s ChartRedraw() is real work that gets slower as more chart objects
// accumulate over a long backtest - calling it every single bar across three passes (instead of
// pass 1 alone, the original frequency) is what actually stalled a run for 1.5+ hours without
// finishing era 0, not the shuffle itself. ~5 updates/sec is still visually live. forceRefresh
// bypasses this - see this method's declaration comment for why the era-end call needs to.
// 2026-07-30 raised 200 -> 400. The second half of the same responsiveness complaint the training
// chunk budget addresses, and on a long-running chart the larger half: ChartRedraw() repaints the
// WHOLE chart, so its cost scales with the accumulated signal arrows, and firing it five times a
// second is what makes dragging the panel stutter rather than any single call being slow. Nothing
// on the simple panel changes fast enough to need 5 Hz - era and accuracy move once per era, and
// the progress percentage is smooth at 2.5 Hz. The era-end call passes forceRefresh and bypasses
// this entirely, so no state transition is ever delayed by it.
const uint STATUS_LABEL_THROTTLE_MS = 400;
uint nowTick = GetTickCount();
if(!forceRefresh && m_lastStatusLabelUpdateTick != 0 && nowTick - m_lastStatusLabelUpdateTick < STATUS_LABEL_THROTTLE_MS)
return;
m_lastStatusLabelUpdateTick = nowTick;
//--- Simple panel (default, VerboseMode off): only what a non-technical user can act on - how far
//--- training has progressed, how reliable its LIVE Buy/Sell calls are (hit-rate = live precision:
//--- the same bars the deployed EA would actually trade, so it is a true forward-trading expectation,
//--- not the Neutral-inflated headline accuracy), and what it is signalling right now. Every raw NN
//--- internal (in-sample error, MSE, softmax neurons, confusion counts, recall) is diagnostic and is
//--- shown only under VerboseMode in the detailed block below.
if(!VerboseMode)
{
ENUM_SIGNAL curSig = m_view.ToSignal(signalValue);
string sigPlain = (curSig == Buy) ? "Buy" : (curSig == Sell) ? "Sell" : "Neutral (no trade)";
//--- Progress of the pass ACTUALLY RUNNING, published by each pass. This used to be computed
//--- here from pass 2's own counters, which during pass 1 read as 100%.
int progressPct = m_view.PassProgressPct();
//--- Compounded, persistent DIRECTIONAL win-rate (Buy/Sell only, Neutral excluded) - a stable
//--- number that keeps refining across eras and restarts, not the noisy per-era metric that
//--- sat at "measuring", and not the Neutral-inflated all-class rate.
string accLine = ComputeCompoundedAccuracyLine();
//--- The checkpoint/deploy internals - best score so far, whether it clears the deploy gate,
//--- eras-since-best, plateau-ladder stage - used to occupy two more panel lines.
string simple = StringFormat(
m_view.DisplayNameForChart() + " - learning (era %d, %s %d%%)\n" +
"%s\n" +
"Current signal: %s",
m_view.EraCount(), m_view.PassLabel(), progressPct, accLine, sigPlain);
m_view.PublishStatus(simple, forceRefresh);
return;
}
// Recall/precision-by-class and the continual-learning OOS sim are still tracked (used for the
// convergence gate elsewhere) but dropped from the on-chart status text - it made the panel too
// tall/wordy for a one-line-per-metric display; the counts below are enough at a glance.
int predBuy = 0, predSell = 0, predNeutral = 0, trueBuy = 0, trueSell = 0, trueNeutral = 0;
m_view.ClassCounts(predBuy, predSell, predNeutral, trueBuy, trueSell, trueNeutral);
string classLine = StringFormat(
"Predicted -> Buy: %d Sell: %d Neutral: %d\n" +
"Actual -> Buy: %d Sell: %d Neutral: %d",
predBuy, predSell, predNeutral,
trueBuy, trueSell, trueNeutral
);
int oosSamples = m_view.OosSamples();
string oosErrStr = (oosSamples > 0 ? DoubleToString(m_view.OosErrorPct(), 2) : "N/A");
//--- THE COMPACT PROGRESSION, carried over from the non-verbose panel above rather than left as
//--- its exclusive feature. progressLine below counts BARS within the running pass, which is the
//--- detail; this answers "how far into the era am I", and it keeps its shape while progressLine
//--- changes wording from pass to pass and reads "Era complete" through a barrier hold.
string eraProgress = StringFormat("%s %d%%", m_view.PassLabel(), m_view.PassProgressPct());
string s;
int outputNeurons = m_view.OutputNeuronsCount();
if(outputNeurons == 1)
s = StringFormat(
m_view.Id() + " : Study -> Era %d (%s)\n" +
"%s\n" +
"IS %d%% Acc: %.2f%% MSE: %.2f AvgErr: %.2f\n" +
"OOS %d%% Acc: %.2f%% Mismatch: %s Samples: %d\n" +
"Signal Neuron: %.5f\n" +
"Forecast: %s -> %.2f\n" +
"%s",
m_view.EraCount(), eraProgress, progressLine,
100 - m_view.OosSplitPct(), m_view.Forecast(),
m_view.ErrorPct(),
m_view.NetRecentAverageError(),
m_view.OosSplitPct(), m_view.OosForecast(), oosErrStr, oosSamples,
neuron0,
EnumToString(m_view.ToSignal(signalValue)), signalValue,
classLine
);
else
if(outputNeurons == 3)
{
int buyRecallPct = -1, sellRecallPct = -1;
m_view.OosRecallPct(buyRecallPct, sellRecallPct);
//--- Headline OOS accuracy above blends in Neutral (usually the majority class, and "don't
//--- trade" rather than a call that can be right or wrong the way Buy/Sell are) - a model
//--- can score well on it while its actual Buy/Sell calls are unreliable.
string oosDirRecall = "OOS recall Buy: " + (buyRecallPct < 0 ? "n/a" : IntegerToString(buyRecallPct) + "%") +
" Sell: " + (sellRecallPct < 0 ? "n/a" : IntegerToString(sellRecallPct) + "%");
int buyPrecPct = -1, buyFired = 0, sellPrecPct = -1, sellFired = 0;
m_view.OosLivePrecision(buyPrecPct, buyFired, sellPrecPct, sellFired);
//--- Live-trade precision line: precision on ONLY the calls that clear the confidence floor under
//--- the live/prior-corrected rule, with the fire count in parentheses. This is the metric that
//--- matches forward trading - the deployed EA takes exactly these bars (see AdjustedSignalFromSoftmax).
string oosLivePrec = "Live win rate Buy: " + (buyPrecPct < 0 ? "n/a" : IntegerToString(buyPrecPct) + "%") +
" (" + IntegerToString(buyFired) + ") Sell: " + (sellPrecPct < 0 ? "n/a" : IntegerToString(sellPrecPct) + "%") +
" (" + IntegerToString(sellFired) + ")";
s = StringFormat(
m_view.Id() + " : Study -> Era %d (%s)\n" +
"%s\n" +
"IS %d%% Acc: %.2f%% MSE: %.2f AvgErr: %.2f\n" +
"OOS %d%% Acc: %.2f%% Mismatch: %s Samples: %d\n" +
"%s\n" +
"%s\n" +
"Buy: %.5f Sell: %.5f Neutral: %.5f\n" +
"Forecast: %s -> %.2f\n" +
"%s",
m_view.EraCount(), eraProgress, progressLine,
100 - m_view.OosSplitPct(), m_view.Forecast(),
m_view.ErrorPct(),
m_view.NetRecentAverageError(),
m_view.OosSplitPct(), m_view.OosForecast(), oosErrStr, oosSamples,
oosDirRecall,
oosLivePrec,
neuron0, neuron1, neuron2,
EnumToString(m_view.ToSignal(signalValue)), signalValue,
classLine
);
}
else
s = "Invalid neuron count!";
m_view.PublishStatus(s, forceRefresh);
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CChartUI::DrawObject(datetime time, double signal, double close)
{
//--- RAW VIEW ONLY. This function draws THIS MODEL's own opinion, undiluted by the vote,
//--- unfiltered by Signal_ThresholdOpen and unranked by the signal DB - which is exactly what
//--- the filtered view must not show.
if(!DrawUnfilteredSignals)
return;
//--- THE TRIGGER PRICE (2026-08-19): the bar's CLOSE, for both sides. This used to be the
//--- candle's low for a Buy and its high for a Sell - prices the trade never touches, chosen so
//--- an arrow glyph would sit clear of the candle.
ENUM_SIGNAL sig = m_view.ToSignal(signal);
if(sig != Buy && sig != Sell)
return;
double price = close;
if(!MathIsValidNumber(price) || price <= 0.0)
return;
string name = m_view.ArrowPrefix() + TimeToString(time);
//--- Deliberately NO ObjectFind() pre-check - same fix as AdvanceChartSignalRestore's ObjectCreate
//--- call (see that method's comment): ObjectFind scans the ENTIRE chart object list, so calling it
//--- once per drawn arrow makes PruneDirectionalClusters's era-end sweep O(n^2) in the arrow count -
//--- exactly the pattern that froze the terminal once already (2026-07-26, arrow restore). This
//--- sweep runs ONCE PER ERA, completely unchunked (no TRAIN_TIME_BUDGET_MS yield), so its cost
//--- directly stalls the panel/chart for however long it takes - raising the training budget can't
//--- fix that, only cutting this O(n^2) cost can. ObjectCreate already returns false harmlessly when
//--- the name exists (ignored below, same as the restore path); re-applying the properties is exactly
//--- what a refresh does regardless of whether the object is new or already there.
//--- the tooltip names the MEMBER: on an ensemble chart four models draw into the same
//--- panel, and an unattributed mark cannot be judged against the model lines above it
//--- DisplayName(), never m_id: the short id is a FOLDER name that outlives display renames (see
//--- SignalHYBRID's "ConvLSTM"/"HYB" pair), so putting it in front of the user relabels the model
//--- on the chart with an internal token - reported 2026-08-16 as marks tagged "HYB" on an
//--- ensemble whose panel line reads "ConvLSTM". The object NAME still uses m_id (it is a
//--- namespace, and renaming it would orphan every persisted mark); only the visible text changes.
WarriorPlotSignalLevel(name, time, m_view.Period(), price, sig == Buy, false,
m_view.DisplayNameForChart() + " " + EnumToString(sig) + " @ " +
DoubleToString(price, m_view.Digits()) + " " + DoubleToString(signal, 5));
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CChartUI::DeleteObject(datetime time)
{
string name = m_view.ArrowPrefix() + TimeToString(time);
//--- No ObjectFind() pre-check either - same O(n^2)-avoidance reasoning as DrawObject() above.
//--- ObjectDelete() already returns false (silently, no dialog) when the name doesn't exist.
WarriorDeleteSignalMark(name);
}
//+------------------------------------------------------------------+
//| End-of-era renderer + non-max suppression over the prediction |
//| cache. This is the SOLE place directional arrows are drawn |
//| during training (the scan passes only RECORD predictions into |
//| the cache - they never draw), so the chart only ever shows the |
//| declustered set, never the raw mid-era clusters. |
//+------------------------------------------------------------------+
void CChartUI::PruneDirectionalClusters(int bars)
{
int clusterWindow = m_view.SignalClusterWindow();
if(clusterWindow <= 0) // NMS off: the passes drew inline, nothing to render here
return;
int cacheSize = m_view.PredictionCacheSize();
if(cacheSize <= 0)
return;
int hi = MathMin(bars, cacheSize);
int lastBuyIdx = -1; // last SEEN bar per direction (same-direction contiguous collapse)
int lastSellIdx = -1;
int keptIdx = -1; // last KEPT bar of EITHER direction (cross-direction resolution)
double keptConf = 0.0;
ENUM_SIGNAL keptDir = Neutral;
for(int idx = hi - 1; idx >= 0; idx--) // high index = oldest bar -> iterate forward in time
{
//--- THE ONE UNCHUNKED SWEEP LEFT, and it scales with history: this function's own header
//--- notes it runs once per era with no TRAIN_TIME_BUDGET_MS yield, so its cost stalls the
//--- chart directly and raising the training budget cannot help.
if(m_view.Stopping())
return;
double sv = m_view.PredictionAt(idx);
if(sv == -2.0) // not scored this era: leave whatever's there
continue;
datetime t = m_view.BarTime(idx);
ENUM_SIGNAL sig = m_view.ToSignal(sv);
if(sig != Buy && sig != Sell) // scored Neutral: ensure no arrow
{
DeleteObject(t);
continue;
}
// 1) Same-direction contiguous collapse: suppress if within the window of the previous SEEN
// same-direction bar; advance last-seen either way so a whole run collapses to its first bar.
int lastSame = (sig == Buy) ? lastBuyIdx : lastSellIdx;
bool sameContinuation = (lastSame >= 0 && (lastSame - idx) <= clusterWindow);
if(sig == Buy)
lastBuyIdx = idx;
else
lastSellIdx = idx;
if(sameContinuation)
{
DeleteObject(t);
continue;
}
// 2) Cross-direction resolution: a fresh cluster within the window of the last KEPT opposite
// signal is flicker at one turn zone (real opposite pivots are a whole leg apart) - keep only the
// higher-confidence side. Confidence = |signed signal| = the winning softmax probability.
double conf = MathAbs(sv);
if(keptIdx >= 0 && keptDir != sig && (keptIdx - idx) <= clusterWindow)
{
if(conf > keptConf)
DeleteObject(m_view.BarTime(keptIdx)); // this bar is stronger: drop the weaker opposite arrow
else
{
DeleteObject(t); // the kept opposite is stronger: suppress this bar
continue;
}
}
// 3) ALTERNATION, identical to NmsLiveAccept's rule 3 and pass 3's. MUST match both: this is the
// drawn history, and an arrow set that does not obey the same rule as the traded set shows the
// user calls the EA would never have taken.
if(m_view.BothDirectionsTradeable() && keptIdx >= 0 && keptDir == sig)
{
DeleteObject(t);
continue;
}
// KEEP: this is the render step - the arrow is drawn here, not by the scan passes.
DrawObject(t, sv, m_view.BarClose(idx));
keptIdx = idx; // first bar of a fresh, window-clear cluster
keptConf = conf;
keptDir = sig;
}
}
//+------------------------------------------------------------------+
//| PurgeChart - Removes this EA's own visual objects from the chart. |
//| Returns how many signal arrows it actually removed. |
//+------------------------------------------------------------------+
int CChartUI::PurgeChart(void)
{
long chartID = 0;
string arrowPrefix = m_view.ArrowPrefix();
//--- Delete ONLY what this EA created: our namespaced signal arrows plus the status-label objects.
//--- Called from the destructor (clean removal); NOT from InitIndicators anymore, so arrows survive
//--- re-inits.
int removed = ObjectsDeleteAll(chartID, arrowPrefix); // THIS member's arrows only
if(removed < 0)
removed = 0;
//--- ...and now everything ELSE this EA owns, through the one shared list (WarriorChartPrefixes).
//--- That is the reported "leftover objects on deinit".
int otherLeft = 0;
int otherRemoved = WarriorPurgeChartObjects(chartID, true, otherLeft);
if(otherRemoved > 0 || otherLeft > 0)
PrintVerbose(m_view.Id() + ": purged " + IntegerToString(otherRemoved) +
" non-arrow EA object(s) (status line / panel)" +
(otherLeft > 0 ? " - " + IntegerToString(otherLeft) + " needed a by-name delete after the bulk call" : ""));
//--- VERIFY, don't assume. When it DOES find something, it finishes the job and says so - naming
//--- the failure instead of leaving it to be re-reported as a visual symptom. WIDENED 2026-08-09.
string leftovers[];
int found = 0;
int objectsTotal = ObjectsTotal(chartID, -1, -1);
int prefixMatches = 0;
if(objectsTotal > 0)
{
ArrayResize(leftovers, objectsTotal);
for(int i = 0; i < objectsTotal; i++)
{
string nm = ObjectName(chartID, i, -1, -1);
if(StringFind(nm, arrowPrefix) != 0)
continue;
prefixMatches++;
leftovers[found++] = nm;
}
}
for(int i = 0; i < found; i++)
ObjectDelete(chartID, leftovers[i]);
if(found > 0)
Print(m_view.Id() + ": WARNING - ObjectsDeleteAll(\"" + arrowPrefix + "\") reported " + IntegerToString(removed) +
" removed but left " + IntegerToString(found) + " signal object(s) on the chart; deleted them by name. " +
"The bulk prefix delete is not doing its job on this terminal build - that is the root cause of any " +
"'arrows stay on the chart' report, not the shutdown ordering.");
//--- The both-zero case is the one that needs saying out loud: it means the chart carried no
//--- objects under our prefix at the moment of the purge, so an "arrows are still on screen" report
//--- cannot be this function's doing and m_lastArrowsSaved must have been counted from somewhere
//--- else (the deferred-restore queue is the one such source - see SaveChartSignals).
if(removed == 0 && found == 0 && m_lastArrowsSaved > 0 && !m_purgeMismatchWarned)
{
m_purgeMismatchWarned = true;
Print(m_view.Id() + ": WARNING - the sidecar was just written with " + IntegerToString(m_lastArrowsSaved) +
" arrow(s) but the chart holds no \"" + arrowPrefix + "\" object at all (" +
IntegerToString(objectsTotal) + " objects of any kind, " + IntegerToString(prefixMatches) +
" matching the prefix). The saved count did NOT come from the chart, so anything still drawn "
"was not created under this prefix and no purge here can remove it.");
}
ClearStatusLabel();
ChartRedraw(chartID);
return removed + found;
}
#endif // WARRIOR_CHART_CHARTUI_MQH
//+------------------------------------------------------------------+