forked from animatedread/Warrior_EA
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>
1006 lines
54 KiB
MQL5
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
|
|
//+------------------------------------------------------------------+
|