BuildFeatureWindow() replaces eight hand-rolled copies of the same loop
and feeds the window OLDEST BAR FIRST. Every copy fed it newest-first,
because MQL5 timeseries indices run backwards and `r + b` with b ascending
walks into the past.
Harmless for PAI and CONV - a dense layer learns a weight per position
either way, a conv learns time-mirrored kernels. Not harmless for the
recurrent stacks:
- LSTM_SeqStepForward reads `inputs + t*Iw`, so step t is block t.
- It writes output[] only when t == steps-1: the visible output IS the
last hidden state.
- c_t = f*c_{t-1} + i*g decays toward the start of the sequence.
lstm_seq_flowcheck.cpp measured block 0's influence on the output at
1.2e-2 of block T-1's, at the shipped forget bias of 1.0.
So the bar being PREDICTED sat at the far end of the decay and the output
was handed to the OLDEST bar in the window - the exact inverse of what the
window is for. ~80x backwards on LSTM and HYBRID, on all three tiers
(OpenCL kernel, CPU DLL, pure-MQL5 inference), which is why it never
surfaced as a backend discrepancy.
This does not create edge - the MI diagnostics read at the noise floor
(p=0.4975) with a working positive control. It makes the one hypothesis
those diagnostics explicitly do NOT cover testable: they are marginal and
per-bar, and state they "cannot rule out one that only exists in
combination or across time". The sequence model is the instrument for
across-time structure and it has been crippled, so that hypothesis has
never been honestly tested.
Fingerprint gets an unconditional |WIN:2 - the vector keeps its shape and
its features, so a stale .nnw would load cleanly and run a model fitted to
one ordering against the other, silently. Re-keying every config is the
point, not collateral damage. FORCES A FULL RETRAIN.
Also: the now-relative bar caches are re-keyed on the two live paths.
EnsureBarCachesCapacity() was only ever called from training paths, but
once m_trainingComplete is set ScheduleTrainingIfNeeded() routes every bar
to RefreshConvergedSignal() and Train() is never re-entered - so nothing
cleared the feature cache again for the life of the process. A chart that
trained to convergence kept replaying the rows computed for the last
training era's bar grid: the live signal froze at its convergence-time
value, and OnlineLearnStep() backpropped those stale features against
freshly resolved labels. Backtests were never affected (an inference-only
process never allocates the arrays, so every read recomputes).
Compiles clean: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
383 lines
22 KiB
MQL5
383 lines
22 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| Warrior_EA |
|
|
//| AnimateDread |
|
|
//| |
|
|
//| Read-time signal production: softmax, prior calibration, class p|
|
|
//| |
|
|
//| PARTIAL IMPLEMENTATION FILE - not standalone. |
|
|
//| This holds CExpertSignalAIBase method BODIES only. The class |
|
|
//| declaration lives in Expert\ExpertSignalAIBase.mqh, which |
|
|
//| #includes this file at the bottom, after the declaration. Do not |
|
|
//| include it anywhere else and do not compile it on its own. |
|
|
//| |
|
|
//| Split out purely to make the 8216-line original navigable; the |
|
|
//| code inside was moved verbatim, not rewritten. |
|
|
//+------------------------------------------------------------------+
|
|
#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/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)
|
|
{
|
|
//--- Size the buffers from what the FEATURE BUILDER actually needs, not from a date delta.
|
|
//--- This used to be `Bars(sym, period, dtStudied, TimeCurrent()) + m_historyBars`. dtStudied is a
|
|
//--- training watermark, and in the Strategy Tester it is loaded from a LIVE-chart save whose
|
|
//--- timestamp is AHEAD of the simulated date - so the interval inverts, Bars() returns ~0, and the
|
|
//--- buffer came out at exactly m_historyBars. That is just deep enough for the OHLC window to
|
|
//--- succeed and far too shallow for the swing-context block behind it: the Donchian-50, the 20-bar
|
|
//--- return and the SMA extension all reach further back than m_historyBars, hit the end of the
|
|
//--- loaded series, and take their graceful degraded path. The result was silent - no error, no short
|
|
//--- window, just inference computing DIFFERENT features from the ones training learned on. Live it
|
|
//--- was the same bug with a milder constant (the delta is ~1 bar, giving m_historyBars + 1).
|
|
//--- SWING_SCAN_CAP_BARS is the deepest lookback any feature performs (FindConfirmedZigZagPivot's
|
|
//--- bound); everything else in BufferTempDataCompute reaches less far.
|
|
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));
|
|
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. Train() is the only other caller of this, and once m_trainingComplete is
|
|
//--- set ScheduleTrainingIfNeeded() routes every subsequent bar HERE instead - Train() is never
|
|
//--- re-entered, so without this call nothing ever clears the cache again for the rest of the process.
|
|
//--- A chart that trained to convergence (or was deployed via DeployNow()) would then keep replaying
|
|
//--- the rows computed for the last training era's bar grid: BufferTempData(0..m_historyBars-1) all hit
|
|
//--- the cache, the feature window never changes, and dPrevSignal freezes at its convergence-time value
|
|
//--- forever - silently, since every buffer above refreshed correctly and the vector is the right SHAPE.
|
|
//--- OnlineLearnStep() below would compound it by backpropping those stale features against freshly
|
|
//--- resolved labels, i.e. actively training the deployed model on mismatched pairs.
|
|
//--- A freshly started inference-only process (a backtest, or a buyer loading a deployed .nnw) was
|
|
//--- never affected: it never allocates these arrays, so BufferTempData()'s `cacheable` test is false
|
|
//--- and it always recomputes. This is a live/forward-chart fix, not a backtest one.
|
|
EnsureBarCachesCapacity(barsNow);
|
|
//--- Same bar grid, same panel. A deployed model never enters Train(), so this is the only place
|
|
//--- its cross-asset panel gets built - and it must be built from the SAME reference set training
|
|
//--- used, or inference reads a different feature vector than the weights were fitted to.
|
|
//--- Only as deep as inference actually reads. RefreshLatestSignal() touches bars 0..m_historyBars-1
|
|
//--- and the panel's own slow window reaches CROSSASSET_SLOW_BARS further back - nothing else. 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. The cache check in
|
|
//--- BuildCrossAssetPanel is >=, so a deeper panel left over from training still satisfies this.
|
|
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);
|
|
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. Runs AFTER the
|
|
//--- live signal is drawn (so the arrow uses the shadow as it was for THIS bar's decision) and BEFORE
|
|
//--- dtStudied advances (OnlineLearnStep keeps its own time watermark, independent of dtStudied).
|
|
OnlineLearnStep();
|
|
dtStudied = m_Time.GetData(0);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void CExpertSignalAIBase::RefreshLatestSignal(void)
|
|
{
|
|
int i = 0;
|
|
//--- Window ends AT (includes) bar i - see Train()'s matching r declaration comment for why: this
|
|
//--- must be the same window Train() learned from, or the deployed model is being queried on a task
|
|
//--- it was never trained for. Both go through BuildFeatureWindow(), which is what guarantees that.
|
|
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()
|
|
return;
|
|
}
|
|
//--- 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. ApplyClassificationSoftmax() computes the softmax INTO TempData and returns
|
|
//--- the decision; AdjustedSignalFromSoftmax() re-reads that same TempData and applies the same
|
|
//--- strict-majority/ties-to-Neutral rule, so since the read-time prior correction was removed
|
|
//--- (2026-07-31) the two provably agree. 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.
|
|
//--- The "raw softmax was neutralized by prior correction" diagnostic that used to sit here went
|
|
//--- with it: with nothing between the two values it could never fire again.
|
|
ApplyClassificationSoftmax();
|
|
dPrevSignal = AdjustedSignalFromSoftmax();
|
|
}
|
|
m_refreshOk++;
|
|
switch(DoubleToSignal(dPrevSignal))
|
|
{
|
|
case Buy:
|
|
m_refreshBuy++;
|
|
break;
|
|
case Sell:
|
|
m_refreshSell++;
|
|
break;
|
|
default:
|
|
m_refreshNeutral++;
|
|
break;
|
|
}
|
|
datetime bt = m_Time.GetData(i);
|
|
//--- Keep a pure inference-side watermark of the newest bar this model has already evaluated. The
|
|
//--- tester may load dtStudied from a live-chart save whose timestamp is AHEAD of the simulated
|
|
//--- backtest date range; using that training watermark to decide whether a "new bar" exists then
|
|
//--- freezes dPrevSignal at its init-bar value for the whole run. m_lastBarTime is this runtime's own
|
|
//--- latest evaluated bar instead, so it stays aligned to whichever history the current process is
|
|
//--- actually traversing.
|
|
m_lastBarTime = bt;
|
|
//--- Live NMS: suppress this newest-bar arrow if a same-direction signal was already kept within
|
|
//--- m_signalClusterWindow bars - the live equivalent of PruneDirectionalClusters' historical sweep,
|
|
//--- so the forward chart declusters the same way the trained history does (see m_signalClusterWindow).
|
|
ENUM_SIGNAL lsig = DoubleToSignal(dPrevSignal);
|
|
if(lsig != Neutral && NmsLiveAccept(bt, lsig, MathAbs(dPrevSignal)))
|
|
DrawObject(bt, dPrevSignal, m_High.GetData(i), m_Low.GetData(i));
|
|
else
|
|
DeleteObject(bt);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
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. That is the failure mode this project has chased repeatedly
|
|
// from the outside (panel says "no directional calls", nobody can tell whether the model is
|
|
// cautious or dead). Detect it here, at the one place the raw logits are first read, and say so.
|
|
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. Buy/Sell now only win with a strict majority over BOTH other classes; every tie,
|
|
// 2-way or 3-way, falls through to Neutral.
|
|
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. Reads the raw softmax probabilities ApplyClassification|
|
|
//| Softmax() left in TempData[0..2] and returns the prior-corrected |
|
|
//| signed decision (+P'(buy)/-P'(sell)/0-neutral), the exact rule |
|
|
//| live trading fires on and the live-fired precision metric scores. |
|
|
//| |
|
|
//| RAW ARGMAX, deliberately. The prior correction this function used |
|
|
//| to apply at read time (Saerens et al. 2002) was REMOVED |
|
|
//| 2026-07-31 along with the AILogitPriorStrength input. |
|
|
//| |
|
|
//| Why there is nothing to correct here: the logit-adjusted loss |
|
|
//| adds tau*log(prior_c) to each class logit inside the TRAINING |
|
|
//| gradient, so the network learns to absorb the offset and its raw |
|
|
//| argmax is ALREADY the balanced-error-optimal decision. Applying a |
|
|
//| second correction at inference would account for the same base |
|
|
//| rate twice and push the decision back toward Neutral - undoing |
|
|
//| exactly what the loss bought. The old code knew this: the whole |
|
|
//| adjustment sat behind an `if(m_useLogitAdjustedLoss) return raw` |
|
|
//| guard and had been unreachable for the entire shipped default |
|
|
//| configuration. Kept as a named function rather than inlined |
|
|
//| because a dozen call sites document themselves by calling "the |
|
|
//| live decision rule" - and that is exactly what this is. |
|
|
//+------------------------------------------------------------------+
|
|
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.
|
|
if(pBuy > pSell && pBuy > pNeutral)
|
|
return pBuy;
|
|
if(pSell > pBuy && pSell > pNeutral)
|
|
return -pSell;
|
|
return 0.0;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| 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.
|
|
//--- THIS USED TO BE SILENT, and that silence hid a whole-run failure: while the auto-tune search ran,
|
|
//--- UpdateClassPriors() was skipped in eval mode, so this branch was taken on EVERY era and the
|
|
//--- imbalance correction never once ran - with nothing in the log to say so. A mechanism that
|
|
//--- declines to act must announce it; the alternative is indistinguishable from working. Third time
|
|
//--- this codebase has been bitten by a quiet no-op, so it now warns every time it is not merely the
|
|
//--- expected era-0 case.
|
|
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. The binding quantity is the SPREAD between the largest and
|
|
//--- smallest offset, not their absolute size: softmax is shift-invariant, so a constant added to
|
|
//--- all three classes changes nothing and only their differences move the decision.
|
|
double lb = MathLog(m_priorBuy), ls = MathLog(m_priorSell), lnn = MathLog(m_priorNeutral);
|
|
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;
|
|
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) + "% | log-prior spread " +
|
|
DoubleToString(spread, 2) + " 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;
|
|
offsets[1] = tauEff * ls;
|
|
offsets[2] = tauEff * lnn;
|
|
Net.SetLogitAdjustment(offsets);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Converts a double to ENUM_SIGNAL. |
|
|
//| 3-output (softmax classification) case: dPrevSignal's *sign* |
|
|
//| alone already encodes the argmax-selected class (+prob for Buy, |
|
|
//| -prob for Sell, exactly 0.0 for Neutral - see Train()/ |
|
|
//| RefreshLatestSignal()), so classification here is pure argmax: |
|
|
//| whichever class the network actually picked, full stop. No |
|
|
//| magnitude threshold is applied - confidence magnitude is a |
|
|
//| separate concern, already exposed via AIConfidence()/ |
|
|
//| SignedAIConfidence() (MathAbs(dPrevSignal)/dPrevSignal) for the |
|
|
//| signal engine's own confidence-weighted filters/lot sizing/SLTP, |
|
|
//| so this keeps "which class" and "how confident" decoupled. |
|
|
//| 1-output (tanh regression) case: unrelated network shape, keeps |
|
|
//| the original 0.50 magnitude cutoff as a genuine confidence gate. |
|
|
//+------------------------------------------------------------------+
|
|
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;
|
|
}
|
|
#endif // WARRIOR_AIBASE_INFERENCE_MQH
|