//+------------------------------------------------------------------+ //| 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 //+------------------------------------------------------------------+