Warrior_EA/System/NewBar.mqh
AnimateDread 6d48fdb4cd perf(tester): stop agents doing chart work on deinit; enforce Expert_EveryTick
Three related changes, all aimed at work being repeated at a frequency
nobody chose.

1. OnDeinit gets a tester/optimizer fast path.

   Everything in the live teardown exists to leave a CHART clean and a live
   model's state on disk. An optimization agent has neither. It was still
   running, on EVERY pass: a per-signal arrow-sidecar WRITE
   (ShutdownChartCleanup -> PersistAndClearChartSignals) plus two full
   chart-object scans plus a ChartRedraw. At optimization scale that is
   hundreds of thousands of pointless file writes per agent, against a
   ~4,500 ms budget MetaTrader force-terminates on - the shape of thing
   that stalls an agent rather than failing it.

   The fast path keeps MarkShutdown() and FlushTrainRun() (so a killed pass
   never leaves a half-written era) and still calls dbm.Deinit() and
   Expert.Deinit() - leaking the signal tree or a handle across passes is
   its own way to accumulate into a stall. The two now-unreachable
   !isTesterRun guards further down are folded away.

2. All four tester handlers are present and documented by WHERE THEY RUN.

   OnTesterInit/OnTesterPass/OnTesterDeinit run in the CONTROLLING TERMINAL
   once per session; only OnTester runs on the agent, per pass. OnTesterPass
   was missing entirely - added empty and deliberately so: it only fires for
   passes that shipped FrameAdd() data, which this EA never sends, and
   reading frames there would put per-pass work on the terminal's critical
   path. Declared so that adding frame-sending later fails loudly instead of
   silently dropping every frame.

3. Expert_EveryTick is now actually enforced.

   It was passed to Expert.Init() and only ever reached StartIndex() - which
   bar a signal READS. The whole pipeline still ran on every quote. It now
   gates m_signal.SetDirection() in CExpertCustom::Processing(): that call
   drives Direction(), which is a TRANSACTION (NN forward passes, DB rows,
   chart arrows, one-shot vote state), and re-running it on every tick of a
   4-hour bar repeats all of it.

   Scoped deliberately. Everything after that line still runs per tick -
   CheckReverse/CheckClose/CheckTrailingStop and pending-order maintenance
   are risk management, and a stop that only trails at bar boundaries is a
   different strategy, not a faster one. The scheduled close-all in OnTick()
   matches a +-1 MINUTE window, so bar-gating it on H4 would step straight
   over the thing 100% of label timeouts already resolve against.
   g_riskBudget.Update() also stays at quote frequency, by design.

   System/NewBar.mqh becomes CNewBar, a class. The free function it replaced
   had zero callers and kept its watermark in a `static`: ONE watermark
   shared by every caller, so the first caller each tick consumed the
   transition and every other caller was told "no new bar" for a bar that
   had just opened. Per-instance state fixes that; first observation counts
   as new, so a fresh attach acts immediately instead of idling up to a full
   bar.

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

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

64 lines
3.4 KiB
MQL5

//+------------------------------------------------------------------+
//| NewBar.mqh |
//| AnimateDread |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "AnimateDread"
#property link "https://www.mql5.com"
#ifndef WARRIOR_SYSTEM_NEWBAR_MQH
#define WARRIOR_SYSTEM_NEWBAR_MQH
//+------------------------------------------------------------------+
//| "HAS A NEW BAR OPENED SINCE I LAST ASKED?" |
//| |
//| A CLASS, not the free function this file used to hold, and the |
//| reason is the one piece of state involved: the last bar time. A |
//| free function has to keep that in a `static`, which makes it ONE |
//| watermark shared by every caller - so the first caller each tick |
//| consumes the transition and every other caller is told "no new |
//| bar" for a bar that just opened. That is silent and direction- |
//| dependent, which is the worst shape a bug can have. One instance |
//| per consumer, each with its own watermark, and the question is |
//| answerable by as many callers as want to ask it. |
//| |
//| FIRST CALL COUNTS AS NEW. A gate built on this runs the moment |
//| the EA attaches rather than sitting idle until the next bar |
//| closes - on H4 that would be up to four hours of a live EA |
//| deliberately doing nothing. |
//+------------------------------------------------------------------+
class CNewBar
{
private:
datetime m_lastTime;
bool m_seen; // false until the first successful read
public:
CNewBar(void) : m_lastTime(0), m_seen(false) {}
//--- Reset the watermark, so the next call reports a new bar again.
void Reset(void) { m_lastTime = 0; m_seen = false; }
//--- The bar time this instance last accepted (0 = never read one).
datetime LastBarTime(void) const { return m_lastTime; }
//--- True exactly once per opened bar of `symbol`/`period` (defaults: the chart's own).
bool IsNewBar(const string symbol = NULL, const ENUM_TIMEFRAMES period = PERIOD_CURRENT)
{
string sym = (symbol == NULL || symbol == "") ? _Symbol : symbol;
datetime cTime[];
ArraySetAsSeries(cTime, true);
//--- CopyTime can fail or come back empty (history not yet synced right after attach, or a gap
//--- on an illiquid symbol). Indexing cTime[0] unguarded would raise "array out of range" and
//--- abort the whole tick. Answering "not a new bar" is the safe direction to fail: the caller
//--- skips this tick's bar-boundary work and asks again on the next one.
if(CopyTime(sym, period, 0, 1, cTime) <= 0 || ArraySize(cTime) == 0)
return false;
if(!m_seen)
{
m_seen = true;
m_lastTime = cTime[0];
return true; // first observation - see the header note
}
if(cTime[0] <= m_lastTime)
return false;
m_lastTime = cTime[0];
return true;
}
};
#endif // WARRIOR_SYSTEM_NEWBAR_MQH