//+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Warrior_EA | //| AnimateDread | //| | //| Read-time signal production: softmax, prior calibration, class p | //+------------------------------------------------------------------+ #ifndef WARRIOR_AIBASE_INFERENCE_MQH #define WARRIOR_AIBASE_INFERENCE_MQH //+------------------------------------------------------------------+ //| Post-convergence "new bar" handler - see ScheduleTrainingIfNeeded()| //| for why this exists: once m_trainingComplete is true, a plain new | //| bar must NOT re-enter Train()'s full era loop (which resets the | //| best-checkpoint/g_eta-decay tracking and runs real Net.backProp() | //| passes again, silently perturbing an already-converged model | //| forever, once per bar, with no way to ever actually finish). This | //| only refreshes the price/indicator buffers and re-runs inference | //| for the newest bar so dPrevSignal/the chart arrow stay current - | //| identical cost to what Train() does per-bar, minus every bit of | //| training (label caching, backProp, checkpointing). | //+------------------------------------------------------------------+ void CExpertSignalAIBase::RefreshConvergedSignal(void) { //--- Meta target: the meta head scores PROPOSED TRADES, not a bare bar window - a candidate-less //--- forward would also be width-mismatched against its input layer (window + descriptor). if(IsMetaTarget()) return; //--- Size the buffers from what the FEATURE BUILDER actually needs, not from a date delta. The //--- result was silent - no error, no short window, just inference computing DIFFERENT features //--- from the ones training learned on. int need = (int)m_historyBars + SWING_SCAN_CAP_BARS + MathMax(m_barrierHorizonBars, 1) + 2; int barsNow = (int)MathMin(need, Bars(m_symbol.Name(), PERIOD_CURRENT)); //--- HOLD RATHER THAN TRADE ON A SHORT WINDOW. `need` is the depth the feature builder requires //--- for inference features to match the ones training learned on; a shallower buffer does not //--- fail, it makes the swing block take its graceful degraded path - which is precisely the //--- silent feature-mismatch this whole `need` calculation was introduced to end (see above). int servable = ServableBars(need, "live inference"); if(servable < need) { if(!m_inferenceDepthRefusalWarned) { m_inferenceDepthRefusalWarned = true; PrintFormat("%s: LIVE INFERENCE HELD - the feature window needs %d bars and the indicators can" " only serve %d. Computing a signal here would silently use the swing block's" " degraded path, i.e. different features from the ones this model was trained on," " so no signal is emitted until the depth is available. See the indicator-cap line" " above for how to raise it.", ID, need, servable); } return; } m_inferenceDepthRefusalWarned = false; if(!ResizeBuffers(barsNow) || !RefreshData()) return; //--- INVALIDATE THE NOW-RELATIVE BAR CACHES. Non-obvious and load-bearing: the feature cache is //--- keyed by MQL5 series index, and index 0 means "newest bar", so every closed candle shifts //--- what every cached row stands for. EnsureBarCachesCapacity(barsNow); //--- Same bar grid, same panel. Only as deep as inference actually reads. Asking for the full //--- `barsNow` here would rebuild a training-depth panel on EVERY bar, which in the tester means //--- one full multi-symbol resample per simulated bar. BuildCrossAssetPanel((int)m_historyBars + CROSSASSET_SLOW_BARS + 2); EnsureSpreadSeries(barsNow); //--- A deployed model never enters Train(), so this is the only place its barrier horizon gets //--- measured - and OnlineLearnStep() below depends on it being right. First call sizes buffers //--- against the fallback, which is harmless: `need` is dominated by SWING_SCAN_CAP_BARS either way. EnsureBarrierHorizon(barsNow); bool refreshed = RefreshLatestSignal(); //--- Continual learning: on a LIVE chart (never the tester/optimizer - OnlineLearnStep() self- //--- guards on m_inferenceOnly) a deployed model keeps adapting to newly-confirmed structure. OnlineLearnStep(); //--- Advance the live new-bar watermark ONLY on success. On failure the gate stays open, so the //--- next tick retries. if(refreshed) dtStudied = m_Time.GetData(0); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool CExpertSignalAIBase::RefreshLatestSignal(void) { //--- Meta target: the live path is the S3 gate (CMetaGate), not the per-bar vote - see //--- RefreshConvergedSignal's meta guard. if(IsMetaTarget()) return false; //--- Bar 1: the newest CLOSED bar, NOT the forming bar. Train() never produces such a window - //--- every labeled bar is fully closed, and its label assumes entry at that bar's CLOSE (see //--- TripleBarrierLabel's header). int i = 1; int r = i; if(!BuildFeatureWindow(r)) { //--- One combined failure now (partial window OR short total) where there used to be two counters. //--- Kept distinct in the tally by testing what actually landed: a window that built every bar but //--- came up short is the "short" case, anything else is a feature-build failure. if(TempData.Total() > 0 && TempData.Total() < (int)m_historyBars * m_neuronsCount) m_refreshFailShort++; else m_refreshFailFeatures++; // see PrintInferenceTally() //--- No opinion this bar rather than a stale one: dPrevSignal still holds the PREVIOUS bar's //--- decision, and LongCondition()/ShortCondition() would keep voting that stale direction //--- all bar. dPrevSignal = 0.0; return false; } //--- Live trading/inference reads from the EMA shadow net, not Net directly - see m_shadowNet's //--- declaration comment. Falls back to Net if the shadow isn't bootstrapped yet (should only be //--- momentarily, on a genuinely fresh start before EnsureShadowNet() has run). EnsureShadowNet(); CNet *deployNet = (CheckPointer(m_shadowNet) != POINTER_INVALID) ? m_shadowNet : Net; deployNet.feedForward(TempData); deployNet.getResults(TempData); if(m_outputNeuronsCount == 1) dPrevSignal = TempData[0]; else if(m_outputNeuronsCount == 3) { //--- Live decision. The call is kept because a dozen sites name it as "the live decision //--- rule" and that is still exactly what it is - the correction now lives in the trained //--- weights instead of here. ApplyClassificationSoftmax(); dPrevSignal = AdjustedSignalFromSoftmax(); } m_refreshOk++; //--- bt anchors the DECISION bar (bar 1, the closed bar the window ends on) - it keys the arrow, //--- its High/Low placement and NMS declustering, and now matches the rescan path, which draws //--- each historical arrow at the bar its window ends on. datetime bt = m_Time.GetData(i); //--- Keep a pure inference-side watermark of the newest bar FRAME this model has already //--- evaluated. m_lastBarTime = m_Time.GetData(0); //--- LIVE NMS, AND IT NOW GATES THE TRADE, NOT JUST THE ARROW. ENUM_SIGNAL lsig = DoubleToSignal(dPrevSignal); bool nmsAccept = (lsig != Neutral) && NmsLiveAccept(bt, lsig, MathAbs(dPrevSignal)); if(lsig != Neutral && !nmsAccept) dPrevSignal = 0.0; // declustered away: no arrow, no vote, no position switch(DoubleToSignal(dPrevSignal)) { case Buy: m_refreshBuy++; break; case Sell: m_refreshSell++; break; default: m_refreshNeutral++; break; } if(nmsAccept) DrawObject(bt, dPrevSignal, m_Close.GetData(i)); else DeleteObject(bt); return true; } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ double CExpertSignalAIBase::ApplyClassificationSoftmax(void) { //--- A non-finite logit poisons everything downstream: maxLogit, every exp(), the sum, and all //--- three probabilities become NaN, and since NaN fails every comparison the two directional //--- tests below are both false - so a NaN'd net returns Neutral on every bar forever and looks //--- EXACTLY like a model that has simply gone quiet. if(!MathIsValidNumber(TempData.At(0)) || !MathIsValidNumber(TempData.At(1)) || !MathIsValidNumber(TempData.At(2))) { static int nanLogitReports = 0; // Bounded: this cannot heal on its own (the weights are already corrupt), so unlimited logging // would fill the journal for as long as the chart stays attached. Three is enough to prove it. if(nanLogitReports < 3) { nanLogitReports++; PrintFormat("%s: %s NON-FINITE network output (%g / %g / %g) - forcing Neutral. The weights are " "corrupt; reload the last good .nnw or reset and retrain. Report %d of 3.", __FUNCTION__, ID, TempData.At(0), TempData.At(1), TempData.At(2), nanLogitReports); } return 0; } // CLASS_LOGIT_SCALE (AI\Network.mqh) must match the training-gradient softmax in // backProp/backPropOCL exactly - this is the same normalization the loss was trained against. double maxLogit = CLASS_LOGIT_SCALE * MathMax(TempData.At(0), MathMax(TempData.At(1), TempData.At(2))); double sum = 0; for(int res = 0; res < 3; res++) { double temp = exp(CLASS_LOGIT_SCALE * TempData.At(res) - maxLogit); sum += temp; TempData.Update(res, temp); } for(int res = 0; res < 3; res++) TempData.Update(res, TempData.At(res) / sum); double pBuy = TempData.At(0); double pSell = TempData.At(1); double pNeutral = TempData.At(2); //--- TempData.Maximum(0,3) scans left-to-right and keeps the FIRST index on a tie, so any tie //--- (including the degenerate all-equal 0.3333/0.3333/0.3333 case from a collapsed/untrained //--- net) always resolved to Buy (index 0) - silently turning "the model has no idea" into a //--- directional trade. if(pBuy > pSell && pBuy > pNeutral) return pBuy; // Buy signal if(pSell > pBuy && pSell > pNeutral) return -pSell; // Sell signal return 0; // Neutral signal (also the fallback on any tie) } //+------------------------------------------------------------------+ //| Post-hoc logit adjustment (prior correction) of the 3-class | //| decision. | //+------------------------------------------------------------------+ double CExpertSignalAIBase::AdjustedSignalFromSoftmax(void) { if(TempData.Total() < 3) return 0.0; double pBuy = TempData.At(0), pSell = TempData.At(1), pNeutral = TempData.At(2); //--- Strict majority, ties to Neutral - the same rule as ApplyClassificationSoftmax(). The returned //--- magnitude is a genuine probability, which the confidence floor and ConfidenceTier() read. bool wantBuy = (pBuy > pSell && pBuy > pNeutral); bool wantSell = (pSell > pBuy && pSell > pNeutral); if(!wantBuy && !wantSell) return 0.0; //--- OPERATING POINT (2026-08-09). Argmax alone answers "which class is most likely"; it does //--- not answer "is this worth trading", and those are different questions whenever the top two //--- classes are nearly tied. if(m_dirConfThreshold > 0.0) { double win = wantBuy ? pBuy : pSell; double rival = wantBuy ? MathMax(pSell, pNeutral) : MathMax(pBuy, pNeutral); if((win - rival) < m_dirConfThreshold) return 0.0; } return wantBuy ? pBuy : -pSell; } //+------------------------------------------------------------------+ //| The statistic the operating point is expressed in - see the | //| declaration. Reads the softmax ALREADY in TempData, so callers | //| must have run ApplyClassificationSoftmax() first. | //+------------------------------------------------------------------+ double CExpertSignalAIBase::DirectionalMargin(void) { if(TempData.Total() < 3) return -1.0; double pBuy = TempData.At(0), pSell = TempData.At(1), pNeutral = TempData.At(2); if(pBuy > pSell && pBuy > pNeutral) return pBuy - MathMax(pSell, pNeutral); if(pSell > pBuy && pSell > pNeutral) return pSell - MathMax(pBuy, pNeutral); return -1.0; // Neutral won: no directional call, so no operating point applies } //+------------------------------------------------------------------+ //| Clear the margin histogram at the start of the calibration walk. | //+------------------------------------------------------------------+ void CExpertSignalAIBase::ResetDirConfHistogram(void) { ArrayInitialize(m_dirConfBinCalls, 0); ArrayInitialize(m_dirConfBinHits, 0); m_dirConfPrimaryBars = 0; } //+------------------------------------------------------------------+ //| One calibration sample. isPrimaryBar survives from when this was | //| harvested inside pass 2's oversampled replay queue, where counting | //| duplicated minority bars would have fitted the operating point to | //| a class balance the live model never sees (the same correction | //| m_cumIsTotal makes - see its note in Training.mqh). The calibration | //| walk visits each bar exactly once and passes true; the parameter | //| stays so any future caller must state which it is. | //+------------------------------------------------------------------+ void CExpertSignalAIBase::AccumulateDirConfSample(double margin, bool wasCorrect, bool isPrimaryBar) { if(!isPrimaryBar) return; //--- Counted BEFORE the directional test: this is the coverage denominator, so it has to be every //--- primary bar the model scored, including the ones it called Neutral. Using only directional //--- bars would make coverage 100% by construction at every threshold. m_dirConfPrimaryBars++; if(margin < 0.0) return; // Neutral won - not a directional call int bin = (int)(margin * DIR_CONF_THRESHOLD_BINS); if(bin < 0) bin = 0; if(bin >= DIR_CONF_THRESHOLD_BINS) bin = DIR_CONF_THRESHOLD_BINS - 1; // margin can reach exactly 1.0 m_dirConfBinCalls[bin]++; if(wasCorrect) m_dirConfBinHits[bin]++; } //+------------------------------------------------------------------+ //| Choose the operating point: the margin at which the model calls | //| a direction as often as a direction actually occurs. One pass, | //| top down, so the running totals are "calls at or above this bin" | //| - the set a threshold there admits. | //+------------------------------------------------------------------+ void CExpertSignalAIBase::FitDirConfThreshold(void) { long totalCalls = 0; for(int b = 0; b < DIR_CONF_THRESHOLD_BINS; b++) totalCalls += m_dirConfBinCalls[b]; if(totalCalls < DIR_CONF_MIN_FIT_CALLS || m_dirConfPrimaryBars <= 0) { //--- Not enough evidence to place an operating point. KEEP THE PREVIOUS ONE - the old //--- behaviour here was to reset to 0.0, which is "call a direction on every bar", the single //--- most exposed setting in the range. if(!m_dirConfSparseWarned) { m_dirConfSparseWarned = true; Print(ID + StringFormat(": directional confidence threshold NOT refitted - only %d directional " "calls in the held-out calibration slice this era (need %d). Keeping " "the previous operating point %.2f; this is normal for the first eras " "and self-corrects as the model starts calling directions.", (int)totalCalls, DIR_CONF_MIN_FIT_CALLS, m_dirConfThreshold)); } return; } //--- THE TARGET IS THE LABEL RATE. Call a direction as often as a direction actually occurs - //--- nothing else. A whole null-of-the-maximum apparatus was then built to hold that down. double targetPct = ScanDirectionalRatePct(); double eraPct = EraDirectionalRatePct(); bool fromScan = (targetPct >= 0.0); if(!fromScan) targetPct = eraPct; // resumed model with no prebuild - the era tally is the only measurement if(targetPct < 0.0) return; // nothing measured either way: keep the operating point we have //--- Kept for the report only. The edge at the chosen point is worth SEEING - it is just no //--- longer what chooses the point. The HORIZON-AWARE one, because this line is what the //--- operator reads to judge a model. double breakEvenPct = EmpiricalBreakEvenPct(); //--- Running "at or above this bin" totals, which is exactly the population a threshold there //--- admits. Coverage therefore rises monotonically as the sweep descends, so |coverage - target| //--- is V-shaped and the first minimum found is the answer. double binCov[DIR_CONF_THRESHOLD_BINS]; double binPrec[DIR_CONF_THRESHOLD_BINS]; long runCalls = 0, runHits = 0; double bestGap = DBL_MAX; int bestBin = -1; for(int b = DIR_CONF_THRESHOLD_BINS - 1; b >= 0; b--) { binCov[b] = 0.0; binPrec[b] = 0.0; runCalls += m_dirConfBinCalls[b]; runHits += m_dirConfBinHits[b]; if(runCalls <= 0) continue; binCov[b] = 100.0 * (double)runCalls / m_dirConfPrimaryBars; binPrec[b] = 100.0 * (double)runHits / runCalls; double gap = MathAbs(binCov[b] - targetPct); //--- Strict <, so a tie keeps the MORE selective bin - the sweep reaches it first. Same //--- tie-break direction the expectancy version used, and for the same reason. if(gap < bestGap) { bestGap = gap; bestBin = b; } } if(bestBin < 0) return; double prevThresh = m_dirConfThreshold; m_dirConfThreshold = (double)bestBin / DIR_CONF_THRESHOLD_BINS; //--- Logged when it moves a bin AND the era cadence is due. The threshold updates every era //--- regardless; only the announcement is throttled. if(TrainLogDue() && MathAbs(m_dirConfThreshold - prevThresh) >= 1.0 / DIR_CONF_THRESHOLD_BINS) Print(ID + StringFormat(": directional confidence threshold %.2f -> %.2f, fitted on CALIBRATION" " over %d held-out bars: calls a direction on %.1f%% of them against a" " %s-measured label rate of %.1f%% (miss %.1fpp - the closest of %d bins)." " Below this winner-vs-rival margin the model abstains." " | Win rate at this point %.1f%% vs %.1f%% horizon-aware break-even" " (edge %.1fpp; geometric %.1f%%) -" " REPORTED, not optimised: the margin does not rank these trades, and" " selecting on that curve is what made this threshold thrash." " | Scan says %.1f%%, era loop says %.1f%% - if these disagree the" " populations differ and the scan is the one the operator reads.", prevThresh, m_dirConfThreshold, (int)m_dirConfPrimaryBars, binCov[bestBin], fromScan ? "scan" : "era-loop", targetPct, bestGap, DIR_CONF_THRESHOLD_BINS, binPrec[bestBin], breakEvenPct, binPrec[bestBin] - breakEvenPct, CostAdjustedBreakEvenPct(), ScanDirectionalRatePct(), eraPct)); } //+------------------------------------------------------------------+ //| EMA-updates the persisted true class base rates from a finished | //| era's true class counts. First real measurement seeds directly; | //| thereafter blended with the same smoothing as the accuracy/ | //| confidence EMAs so one noisy era can't swing the live decision. | //+------------------------------------------------------------------+ void CExpertSignalAIBase::UpdateClassPriors(long buyCnt, long sellCnt, long neutralCnt) { long tot = buyCnt + sellCnt + neutralCnt; if(tot <= 0) return; double pb = (double)buyCnt / tot, ps = (double)sellCnt / tot, pn = (double)neutralCnt / tot; if(m_priorNeutral <= 0.0) // first real measurement { m_priorBuy = pb; m_priorSell = ps; m_priorNeutral = pn; return; } //--- (Was `m_useStaticPrior || m_freezePriorCalibration`. Those were two separate user-facing inputs //--- whose only effect anywhere in the codebase was this one OR - two controls for one decision. //--- UseStaticPrior was removed 2026-07-31; see the class-imbalance audit in Variables\Inputs.mqh.) if(m_freezePriorCalibration) return; double k = Net.recentAverageSmoothingFactor; if(k < 1.0) k = 1.0; m_priorBuy += (pb - m_priorBuy) / k; m_priorSell += (ps - m_priorSell) / k; m_priorNeutral += (pn - m_priorNeutral) / k; } //+------------------------------------------------------------------+ //| Installs the training-time logit offsets - see the declaration. | //+------------------------------------------------------------------+ void CExpertSignalAIBase::ApplyLogitAdjustment(void) { if(CheckPointer(Net) == POINTER_INVALID) return; if(m_logitAdjustTau <= 0.0) { //--- Clear rather than merely skip: the input can be turned off on a chart that already installed //--- offsets this session, and a stale adjustment would keep biasing the gradient silently. Net.ClearLogitAdjustment(); return; } //--- Priors not measured yet (era 0 before the first tally, or a model with no .stats): leave //--- the gradient unadjusted rather than guessing a distribution. The next era installs them. if(m_priorBuy <= 0.0 || m_priorSell <= 0.0 || m_priorNeutral <= 0.0) { if(m_eraCount > 0 && !m_logitAdjustSkipWarned) { m_logitAdjustSkipWarned = true; Print(ID + ": WARNING - class-imbalance correction is NOT running at era " + IntegerToString(m_eraCount) + ": the class priors have never been measured (Buy " + DoubleToString(m_priorBuy, 4) + " Sell " + DoubleToString(m_priorSell, 4) + " Neutral " + DoubleToString(m_priorNeutral, 4) + "). Training is falling back to plain cross-entropy, " "which on a skewed label set collapses to the majority class."); } Net.ClearLogitAdjustment(); return; } //--- Effective tau, capped so the offsets cannot swamp the head's usable logit range - see //--- LOGIT_ADJUST_MAX_RANGE_FRACTION. double lb = MathLog(m_priorBuy), ls = MathLog(m_priorSell), lnn = MathLog(m_priorNeutral); //--- ALL THREE CLASSES, WITH ONE INVARIANT: THE ABSTAIN CLASS IS NEVER SUBSIDISED. double mid = (lb + ls + lnn) / 3.0; double spread = MathMax(lb, MathMax(ls, lnn)) - MathMin(lb, MathMin(ls, lnn)); double tauEff = m_logitAdjustTau; if(spread > 0.0) { double cap = LOGIT_ADJUST_MAX_RANGE_FRACTION * CLASS_LOGIT_SCALE / spread; if(tauEff > cap) tauEff = cap; } if(!m_logitAdjustLogged) { m_logitAdjustLogged = true; //--- Says which way the abstain class is being pushed, because that is the whole question this //--- correction has got wrong in both directions before. Print(ID + ": logit adjustment - measured priors Buy " + DoubleToString(m_priorBuy * 100.0, 2) + "% Sell " + DoubleToString(m_priorSell * 100.0, 2) + "% Neutral " + DoubleToString(m_priorNeutral * 100.0, 2) + "% | APPLIED across all three, log-prior" " spread " + DoubleToString(spread, 2) + " | Neutral offset " + DoubleToString(MathMax(tauEff * (lnn - mid), 0.0), 2) + (m_priorNeutral < m_priorBuy && m_priorNeutral < m_priorSell ? " - CLAMPED to zero: Neutral is the RAREST class here, and paying the model to abstain is" " the 2026-08-16 collapse. Uncapped it would have been " + DoubleToString(tauEff * (lnn - mid), 2) : " - Neutral is over-represented, so this PENALISES abstention") + " against a logit range of " + DoubleToString(CLASS_LOGIT_SCALE, 1) + " | tau " + DoubleToString(m_logitAdjustTau, 2) + (tauEff < m_logitAdjustTau ? " CAPPED to " + DoubleToString(tauEff, 2) + " (uncapped it would consume " + DoubleToString(100.0 * spread * m_logitAdjustTau / CLASS_LOGIT_SCALE, 0) + "% of the range and saturate the head)" : " (uncapped - within budget)")); } //--- ORDERED to match the output layer: [0]=Buy, [1]=Sell, [2]=Neutral - the order //--- BuildFreshTopology emits and the order the softmax gradient reads (AI\Network.mqh). double offsets[3]; offsets[0] = tauEff * (lb - mid); offsets[1] = tauEff * (ls - mid); //--- A NEGATIVE offset here is a subsidy: it makes the head produce a larger raw Neutral logit to //--- classify Neutral correctly, which is exactly what wins Neutral more bars at inference. Clamped //--- away. A positive one penalises over-abstention and is allowed through. offsets[2] = MathMax(tauEff * (lnn - mid), 0.0); Net.SetLogitAdjustment(offsets); } //+------------------------------------------------------------------+ //| Converts a double to ENUM_SIGNAL. | //+------------------------------------------------------------------+ ENUM_SIGNAL CExpertSignalAIBase::DoubleToSignal(double value) { value = NormalizeDouble(value, 2); // Round 'value' to two decimal places if(value < -1.0 || value > 1.0) return Undefine; // out of range, e.g. the -2 "not yet studied" sentinel if(m_outputNeuronsCount == 3) { if(value > 0.0) return Buy; if(value < 0.0) return Sell; return Neutral; } if(value > 0.50) return Buy; if(value < -0.50) return Sell; return Neutral; } //+------------------------------------------------------------------+ //| Throttled, SIDE-EFFECT-FREE forward of the current decision bar, | //| for display only (the HUD member lines and the prospective | //| vote). | //+------------------------------------------------------------------+ //--- (uint) so the throttle comparison is unsigned-vs-unsigned: age is a uint tick delta, and //--- the ternary picking between these is a runtime expression the compiler cannot constant- //--- fold, so bare int literals here drew a sign-mismatch warning (reported 2026-08-19). #define DISPLAY_FWD_MIN_MS ((uint)4000) #define DISPLAY_FWD_ERA_MS ((uint)1000) bool CExpertSignalAIBase::DisplayInference(void) { //--- Meta head consumes fired candidates, not a bare bar window - a candidate-less forward is //--- width-mismatched against its input layer. Same guard as RefreshConvergedSignal. if(IsMetaTarget()) return false; if(CheckPointer(Net) == POINTER_INVALID) return false; if(m_outputNeuronsCount != 1 && m_outputNeuronsCount != 3) return false; uint now = GetTickCount(); uint age = now - m_dispStamp; // unsigned subtraction survives the 49-day wrap bool eraMoved = ((long)m_eraCount != m_dispEra); if(m_dispStamp != 0 && age < (eraMoved ? DISPLAY_FWD_ERA_MS : DISPLAY_FWD_MIN_MS)) return m_dispValid; // serve the cache (or keep failing quietly) until the throttle opens m_dispStamp = now; if(!BuildFeatureWindow(1)) return m_dispValid; // window not buildable (warm-up, indicator hole): keep the last read //--- Save/restore, NOT set/clear - see the header. Frozen, this forward is a pure function. bool bnWasFrozen = Net.GetBatchNormFrozen(); if(!bnWasFrozen) Net.SetBatchNormFrozen(true); bool fwdOk = Net.feedForward(TempData); if(fwdOk) Net.getResults(TempData); if(!bnWasFrozen) Net.SetBatchNormFrozen(false); if(!fwdOk) return m_dispValid; if(m_outputNeuronsCount == 1) { double v = TempData.At(0); if(!MathIsValidNumber(v)) return m_dispValid; // NaN net: keep the last finite read, the NaN latch reports elsewhere m_dispProbs[0] = v; m_dispProbs[1] = 0.0; m_dispProbs[2] = 0.0; m_dispSignal = v; } else { //--- Same two calls, same order, as the live decision in RefreshLatestSignal: softmax INTO //--- TempData, then the strict-majority read. ApplyClassificationSoftmax(); double p0 = TempData.At(0), p1 = TempData.At(1), p2 = TempData.At(2); if(!MathIsValidNumber(p0) || !MathIsValidNumber(p1) || !MathIsValidNumber(p2)) return m_dispValid; m_dispProbs[0] = p0; // Buy m_dispProbs[1] = p1; // Sell m_dispProbs[2] = p2; // Neutral m_dispSignal = AdjustedSignalFromSoftmax(); } m_dispEra = (long)m_eraCount; m_dispValid = true; return true; } #endif // WARRIOR_AIBASE_INFERENCE_MQH