2026-08-01 11:27:28 -04:00
//+------------------------------------------------------------------+
//| NetForward.mqh |
//| |
//| CNet inference and training passes: feedForward, backProp, |
//| getResults, logit adjustment. |
//| |
//| Included from AI\Network.mqh AFTER every class declaration - |
//| bodies only, no declarations. Relocation is behaviour-neutral by |
//| construction: nothing here is reachable until Network.mqh ends. |
//+------------------------------------------------------------------+
# ifndef WARRIOR_AI_IMPL_NETFORWARD_MQH
# define WARRIOR_AI_IMPL_NETFORWARD_MQH
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool CNet : : feedForward ( CArrayDouble * inputVals )
{
if ( CheckPointer ( layers ) = = POINTER_INVALID | | CheckPointer ( inputVals ) = = POINTER_INVALID | | layers . Total ( ) < = 1 )
return false ;
//--- Pure-MQL5 inference: OCL-format neurons loaded host-only, computed in double precision. Separate
//--- path because the branches below assume either a device backend or the plain CNeuronBase model.
if ( m_cpuInference )
return feedForwardCPU ( inputVals ) ;
//---
CLayer * previous = NULL ;
CLayer * current = layers . At ( 0 ) ;
int total = MathMin ( current . Total ( ) , inputVals . Total ( ) ) ;
CNeuronBase * neuron = NULL ;
bool gpuActive = ( CheckPointer ( opencl ) ! = POINTER_INVALID | | CheckPointer ( directml ) ! = POINTER_INVALID ) ;
if ( ! gpuActive )
{
for ( int i = 0 ; i < total ; i + + )
{
neuron = current . At ( i ) ;
if ( CheckPointer ( neuron ) = = POINTER_INVALID )
return false ;
neuron . setOutputVal ( inputVals . At ( i ) ) ;
}
}
else
{
CNeuronBaseOCL * neuron_ocl = current . At ( 0 ) ;
int total_data = inputVals . Total ( ) ;
bool written ;
if ( CheckPointer ( opencl ) ! = POINTER_INVALID )
{
//--- OpenCL device buffers are float32 (see AI\Network.cl) - unlike CBufferDouble's own
//--- BufferWrite(), this call bypasses that class entirely (it writes straight into the
//--- first layer's Output buffer by index), so the narrow-to-float has to happen here too.
float array [ ] ;
if ( ArrayResize ( array , total_data ) < 0 )
return false ;
for ( int d = 0 ; d < total_data ; d + + )
array [ d ] = ( float ) inputVals . At ( d ) ;
written = opencl . BufferWrite ( neuron_ocl . getOutputIndex ( ) , array , 0 , 0 , total_data ) ;
}
else
{
double array [ ] ;
if ( ArrayResize ( array , total_data ) < 0 )
return false ;
for ( int d = 0 ; d < total_data ; d + + )
array [ d ] = inputVals . At ( d ) ;
written = directml . BufferWrite ( neuron_ocl . getOutputIndex ( ) , array , total_data ) ;
}
if ( ! written )
return false ;
}
//---
CObject * temp = NULL ;
for ( int l = 1 ; l < layers . Total ( ) ; l + + )
{
previous = current ;
current = layers . At ( l ) ;
if ( CheckPointer ( current ) = = POINTER_INVALID )
return false ;
//---
if ( gpuActive )
{
CNeuronBaseOCL * current_ocl = current . At ( 0 ) ;
if ( ! current_ocl . feedForward ( previous . At ( 0 ) ) )
return false ;
continue ;
}
//---
total = current . Total ( ) ;
if ( current . At ( 0 ) . Type ( ) = = defNeuron )
total - - ;
//---
for ( int n = 0 ; n < total ; n + + )
{
neuron = current . At ( n ) ;
if ( CheckPointer ( neuron ) = = POINTER_INVALID )
return false ;
if ( previous . At ( 0 ) . Type ( ) = = defNeuron )
{
temp = previous ;
if ( ! neuron . feedForward ( temp ) )
return false ;
continue ;
}
if ( neuron . Type ( ) = = defNeuron )
{
if ( n = = 0 )
{
CLayer * temp_l = new CLayer ( total ) ;
if ( CheckPointer ( temp_l ) = = POINTER_INVALID )
return false ;
CNeuronPool * Pool = NULL ;
for ( int p = 0 ; p < previous . Total ( ) ; p + + )
{
Pool = previous . At ( p ) ;
if ( CheckPointer ( Pool ) = = POINTER_INVALID )
return false ;
temp_l . AddArray ( Pool . getOutputLayer ( ) ) ;
}
temp = temp_l ;
}
if ( ! neuron . feedForward ( temp ) )
return false ;
if ( n = = total - 1 )
{
CLayer * temp_l = temp ;
temp_l . FreeMode ( false ) ;
temp_l . Shutdown ( ) ;
delete temp_l ;
}
continue ;
}
temp = previous . At ( n ) ;
if ( CheckPointer ( temp ) = = POINTER_INVALID )
return false ;
if ( ! neuron . feedForward ( temp ) )
return false ;
}
}
//---
return true ;
}
//+------------------------------------------------------------------+
//| Pure-MQL5 forward pass over an OCL-format network loaded host-only|
//| (no OpenCL/DirectML/DLL). Layer 0 is fed from inputVals; each |
//| subsequent layer's OCL neuron computes via its virtual |
//| feedForwardCPU() (dense/conv/pool/LSTM). See SetCpuInference(). |
//+------------------------------------------------------------------+
bool CNet : : feedForwardCPU ( CArrayDouble * inputVals )
{
CLayer * current = layers . At ( 0 ) ;
if ( CheckPointer ( current ) = = POINTER_INVALID )
return false ;
CNeuronBaseOCL * in0 = current . At ( 0 ) ;
if ( CheckPointer ( in0 ) = = POINTER_INVALID | | ! in0 . SetInputsCPU ( inputVals ) )
return false ;
for ( int l = 1 ; l < layers . Total ( ) ; l + + )
{
CLayer * previous = current ;
current = layers . At ( l ) ;
if ( CheckPointer ( current ) = = POINTER_INVALID )
return false ;
CNeuronBaseOCL * cur = current . At ( 0 ) ;
CNeuronBaseOCL * prev = previous . At ( 0 ) ;
if ( CheckPointer ( cur ) = = POINTER_INVALID | | CheckPointer ( prev ) = = POINTER_INVALID )
return false ;
if ( ! cur . feedForwardCPU ( prev ) )
return false ;
}
return true ;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CNet : : backProp ( CArrayDouble * targetVals , double sampleWeight )
{
if ( CheckPointer ( targetVals ) = = POINTER_INVALID | | CheckPointer ( layers ) = = POINTER_INVALID )
return ;
if ( CheckPointer ( opencl ) ! = POINTER_INVALID | | CheckPointer ( directml ) ! = POINTER_INVALID )
{
backPropOCL ( targetVals , sampleWeight ) ;
return ;
}
//---
CLayer * outputLayer = layers . At ( layers . Total ( ) - 1 ) ;
if ( CheckPointer ( outputLayer ) = = POINTER_INVALID )
return ;
//---
//--- Defensive: this is the pure-MQL5 CPU backward pass and it walks CNeuron/CNeuronBase objects
//--- with SCALAR getOutputVal()/getGradient()/setGradient() accessors. CNeuronBaseOCL & friends do
//--- NOT derive from CNeuronBase and expose only ARRAY accessors over a device buffer, so they can
//--- never be walked here. That is normally impossible to reach: the early return above hands any
//--- backend-backed net to backPropOCL(), and CNet::Create() only ever constructs OCL neurons when
//--- a backend exists. The one way to hold OCL neurons with no backend is pure-MQL5 inference mode
//--- (SetCpuInference -> CLayer::CreateElementScaled's host-only branch), which is inference-only
//--- and never backprops (OnlineLearnStep() guards on Net.CpuInference()). Bail out loudly rather
//--- than mis-cast if that ever changes.
//--- 2026-07-28: a set of inline "handle OCL neuron types" branches was added throughout this
//--- function to cover that impossible case. They could not compile - they called the scalar
//--- accessors on CNeuronBaseOCL - and were removed; this guard replaces them.
CObject * probe = outputLayer . At ( 0 ) ;
if ( CheckPointer ( probe ) ! = POINTER_INVALID )
{
int t0 = probe . Type ( ) ;
if ( t0 = = defNeuronBaseOCL | | t0 = = defNeuronConvOCL | | t0 = = defNeuronPoolOCL | | t0 = = defNeuronLSTMOCL | |
t0 = = defNeuronBatchNormOCL )
{
Print ( __FUNCTION__ + " : REFUSED - CPU backward pass reached a net built from OpenCL/DirectML neurons with no compute backend attached. Nothing was trained this step. " ) ;
return ;
}
}
//---
double error = 0.0 ;
int total = outputLayer . Total ( ) - 1 ;
//--- 3-output classification case: true softmax + categorical-cross-entropy gradient
//--- (dL/dz_i = softmax_i - target_i, the standard multi-class formula - see nnbook.txt section
//--- 1.4) instead of 3 independent per-neuron sigmoid deltas. Forward activation stays SIGMOID
//--- (bounded, avoids the historical logit-runaway collapse documented at
//--- BuildFreshTopology()'s desc.activation comment in ExpertSignalAIBase.mqh), but the BACKWARD
//--- delta is now computed from the softmax-normalized probability across all 3 outputs jointly,
//--- not each neuron's own raw sigmoid value in isolation. This is what actually ties Buy/Sell/
//--- Neutral together during training: raising one class's softmax probability now structurally
//--- lowers the other two's (via the shared normalizing sum), giving real competition instead of
//--- three independent binary regressions that can all drift toward "predict Neutral" together -
//--- root cause of the "overshoot to all-Neutral" convergence failure this replaces.
feat: S2 meta-labeling head - binary trade-quality model over the classic-candidate corpus
The NN now has a target that is not per-bar direction (closed, best-of-999
p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of
cost). One net for all 52 pattern-sides, AIType=AI_META.
- NetForward.mqh: the host-side softmax+CE gradient generalized total==3 ->
2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no
compute backend changes.
- SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on
disk (decoupled from the config fingerprint that burned four S1 runs); the
GMT->server offset is measured PER ROW against entryPrice vs bar open
(DST-immune, histogram logged); a window-span regime filter drops the
pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the
input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR).
- Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate
calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell
lets checkpoint selection, the edge floor, the plateau ladder and the
family-wise deploy gate run UNCHANGED: precision reads as win rate among
traded candidates, chance as the base win rate, recalls as sensitivity/
specificity. Era-end META line: coverage x (p - break-even) vs the null.
- Labels are the side-conditional triple-barrier win caches - never the DB's
stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base
rate). Live inference + online learning guarded off until S3.
- Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename
slot keep meta models fully separate from direction models.
Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with
AIType=AI_META; S3 wires the votes via the per-side hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 06:52:31 -04:00
//--- 2 outputs = the binary meta-labeling head (win/loss - see Meta_Labeling_Design.md S2). A
//--- 2-class softmax+CE is mathematically identical to a logistic/BCE head (the logit difference
//--- is the log-odds), so the binary target reuses this exact machinery instead of growing a
//--- separate BCE path. dLogitAdjust is only ever installed for the 3-class direction head
//--- (SetLogitAdjustment's callers), so the n<total read below never touches a stale slot.
bool useSoftmaxGrad = ( total = = 3 | | total = = 2 ) ;
2026-08-01 11:27:28 -04:00
double smax [ 3 ] ;
if ( useSoftmaxGrad )
{
//--- Logit adjustment added BEFORE the max-subtraction so the shift stays numerically safe.
double logit [ 3 ] ;
double maxLogit = - DBL_MAX ;
feat: S2 meta-labeling head - binary trade-quality model over the classic-candidate corpus
The NN now has a target that is not per-bar direction (closed, best-of-999
p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of
cost). One net for all 52 pattern-sides, AIType=AI_META.
- NetForward.mqh: the host-side softmax+CE gradient generalized total==3 ->
2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no
compute backend changes.
- SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on
disk (decoupled from the config fingerprint that burned four S1 runs); the
GMT->server offset is measured PER ROW against entryPrice vs bar open
(DST-immune, histogram logged); a window-span regime filter drops the
pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the
input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR).
- Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate
calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell
lets checkpoint selection, the edge floor, the plateau ladder and the
family-wise deploy gate run UNCHANGED: precision reads as win rate among
traded candidates, chance as the base win rate, recalls as sensitivity/
specificity. Era-end META line: coverage x (p - break-even) vs the null.
- Labels are the side-conditional triple-barrier win caches - never the DB's
stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base
rate). Live inference + online learning guarded off until S3.
- Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename
slot keep meta models fully separate from direction models.
Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with
AIType=AI_META; S3 wires the votes via the per-side hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 06:52:31 -04:00
for ( int n = 0 ; n < total ; n + + )
2026-08-01 11:27:28 -04:00
{
CNeuron * nrn = outputLayer . At ( n ) ;
logit [ n ] = CLASS_LOGIT_SCALE * nrn . getOutputVal ( ) + ( bLogitAdjust ? dLogitAdjust [ n ] : 0.0 ) ;
maxLogit = MathMax ( maxLogit , logit [ n ] ) ;
}
double sum = 0.0 ;
feat: S2 meta-labeling head - binary trade-quality model over the classic-candidate corpus
The NN now has a target that is not per-bar direction (closed, best-of-999
p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of
cost). One net for all 52 pattern-sides, AIType=AI_META.
- NetForward.mqh: the host-side softmax+CE gradient generalized total==3 ->
2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no
compute backend changes.
- SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on
disk (decoupled from the config fingerprint that burned four S1 runs); the
GMT->server offset is measured PER ROW against entryPrice vs bar open
(DST-immune, histogram logged); a window-span regime filter drops the
pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the
input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR).
- Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate
calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell
lets checkpoint selection, the edge floor, the plateau ladder and the
family-wise deploy gate run UNCHANGED: precision reads as win rate among
traded candidates, chance as the base win rate, recalls as sensitivity/
specificity. Era-end META line: coverage x (p - break-even) vs the null.
- Labels are the side-conditional triple-barrier win caches - never the DB's
stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base
rate). Live inference + online learning guarded off until S3.
- Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename
slot keep meta models fully separate from direction models.
Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with
AIType=AI_META; S3 wires the votes via the per-side hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 06:52:31 -04:00
for ( int n = 0 ; n < total ; n + + )
2026-08-01 11:27:28 -04:00
{
smax [ n ] = exp ( logit [ n ] - maxLogit ) ;
sum + = smax [ n ] ;
}
feat: S2 meta-labeling head - binary trade-quality model over the classic-candidate corpus
The NN now has a target that is not per-bar direction (closed, best-of-999
p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of
cost). One net for all 52 pattern-sides, AIType=AI_META.
- NetForward.mqh: the host-side softmax+CE gradient generalized total==3 ->
2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no
compute backend changes.
- SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on
disk (decoupled from the config fingerprint that burned four S1 runs); the
GMT->server offset is measured PER ROW against entryPrice vs bar open
(DST-immune, histogram logged); a window-span regime filter drops the
pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the
input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR).
- Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate
calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell
lets checkpoint selection, the edge floor, the plateau ladder and the
family-wise deploy gate run UNCHANGED: precision reads as win rate among
traded candidates, chance as the base win rate, recalls as sensitivity/
specificity. Era-end META line: coverage x (p - break-even) vs the null.
- Labels are the side-conditional triple-barrier win caches - never the DB's
stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base
rate). Live inference + online learning guarded off until S3.
- Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename
slot keep meta models fully separate from direction models.
Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with
AIType=AI_META; S3 wires the votes via the per-side hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 06:52:31 -04:00
for ( int n = 0 ; n < total ; n + + )
2026-08-01 11:27:28 -04:00
smax [ n ] / = sum ;
}
for ( int n = 0 ; n < total & & ! IsStopped ( ) ; n + + )
{
CNeuron * neuron = outputLayer . At ( n ) ;
double target = targetVals . At ( n ) ;
double clampedTarget = ( target > 1 ? 1 : target < -1 ? -1 : target ) ;
double delta = clampedTarget - neuron . getOutputVal ( ) ;
error + = delta * delta ;
if ( useSoftmaxGrad )
neuron . setGradient ( clampedTarget - smax [ n ] ) ;
else
neuron . calcOutputGradients ( targetVals . At ( n ) ) ;
//--- inverse-class-frequency loss weighting (see ExpertSignalAIBase.mqh's Train() for how
//--- sampleWeight is derived) - scales the just-computed output gradient in place, before the
//--- hidden layers below read it via sumDOW(), so the whole backward chain sees the weighted
//--- signal without needing its own separate weighting logic.
if ( sampleWeight ! = 1.0 )
neuron . setGradient ( neuron . getGradient ( ) * sampleWeight ) ;
}
error / = total ;
error = sqrt ( error ) ;
recentAverageError + = ( error - recentAverageError ) / recentAverageSmoothingFactor ;
//---
CNeuronBase * neuron = NULL ;
CObject * temp = NULL ;
for ( int layerNum = layers . Total ( ) - 2 ; layerNum > 0 ; layerNum - - )
{
CLayer * hiddenLayer = layers . At ( layerNum ) ;
CLayer * nextLayer = layers . At ( layerNum + 1 ) ;
total = hiddenLayer . Total ( ) ;
for ( int n = 0 ; n < total & & ! IsStopped ( ) ; + + n )
{
neuron = hiddenLayer . At ( n ) ;
if ( nextLayer . At ( 0 ) . Type ( ) = = defNeuron )
{
temp = nextLayer ;
neuron . calcHiddenGradients ( temp ) ;
continue ;
}
if ( neuron . Type ( ) = = defNeuron )
{
double g = 0 ;
for ( int i = 0 ; i < nextLayer . Total ( ) ; i + + )
{
temp = nextLayer . At ( i ) ;
neuron . calcHiddenGradients ( temp ) ;
g + = neuron . getGradient ( ) ;
}
neuron . setGradient ( g ) ;
continue ;
}
temp = nextLayer . At ( n ) ;
neuron . calcHiddenGradients ( temp ) ;
}
}
//---
for ( int layerNum = layers . Total ( ) - 1 ; layerNum > 0 ; layerNum - - )
{
CLayer * layer = layers . At ( layerNum ) ;
CLayer * prevLayer = layers . At ( layerNum - 1 ) ;
total = layer . Total ( ) - ( layer . At ( 0 ) . Type ( ) = = defNeuron ? 1 : 0 ) ;
int n_conv = 0 ;
for ( int n = 0 ; n < total & & ! IsStopped ( ) ; n + + )
{
neuron = layer . At ( n ) ;
if ( CheckPointer ( neuron ) = = POINTER_INVALID )
return ;
if ( neuron . Type ( ) = = defNeuronPool )
continue ;
switch ( prevLayer . At ( 0 ) . Type ( ) )
{
case defNeuron :
temp = prevLayer ;
neuron . updateInputWeights ( temp ) ;
break ;
case defNeuronConv :
case defNeuronPool :
case defNeuronLSTM :
if ( neuron . Type ( ) = = defNeuron )
{
for ( n_conv = 0 ; n_conv < prevLayer . Total ( ) ; n_conv + + )
{
temp = prevLayer . At ( n_conv ) ;
neuron . updateInputWeights ( temp ) ;
}
}
else
{
temp = prevLayer . At ( n ) ;
neuron . updateInputWeights ( temp ) ;
}
break ;
default :
temp = NULL ;
break ;
}
}
}
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CNet : : backPropOCL ( CArrayDouble * targetVals , double sampleWeight )
{
if ( CheckPointer ( targetVals ) = = POINTER_INVALID | | CheckPointer ( layers ) = = POINTER_INVALID | |
( CheckPointer ( opencl ) = = POINTER_INVALID & & CheckPointer ( directml ) = = POINTER_INVALID ) )
return ;
CLayer * currentLayer = ( CLayer * ) layers . At ( layers . Total ( ) - 1 ) ;
if ( CheckPointer ( currentLayer ) = = POINTER_INVALID )
return ;
//---
double error = 0.0 ;
int total = targetVals . Total ( ) ;
double result [ ] ;
CNeuronBaseOCL * neuron = ( CNeuronBaseOCL * ) currentLayer . At ( 0 ) ;
if ( neuron . getOutputVal ( result ) < total )
return ;
for ( int n = 0 ; n < total & & ! IsStopped ( ) ; n + + )
{
double target = targetVals . At ( n ) ;
// Deliberately NOT special-cased on target==0 (an earlier version zeroed delta whenever
// target==0, so a one-hot classification target only ever counted the true class's own
// error - e.g. a Neutral-labeled bar's Buy/Sell neurons were invisible to this metric,
// which is what CNet::backProp()'s CPU-fallback path computes for every output
// unconditionally, and is what actually drives the dError<0.1 convergence gate in
// ExpertSignalAIBase::Train(). The real gradient (CPU_CalcOutputGradient in WarriorCPU.cpp
// / DirectML's equivalent) was never affected - only this diagnostic/convergence metric was.
double delta = ( target > 1 ? 1 : target < -1 ? -1 : target ) - result [ n ] ;
error + = MathPow ( delta , 2 ) ;
}
error / = total ;
error = sqrt ( error ) ;
recentAverageError + = ( error - recentAverageError ) / recentAverageSmoothingFactor ;
if ( ! neuron . calcOutputGradients ( targetVals ) )
return ;
//--- 3-output classification case: overwrite the native per-neuron sigmoid delta with the true
//--- softmax + categorical-cross-entropy gradient (softmax_i - target_i), computed here in MQL5
//--- from the raw outputs already read back into result[] above - see the matching CNet::backProp()
//--- (CPU fallback) comment for the full rationale (ties Buy/Sell/Neutral together via the shared
//--- softmax normalizer instead of training 3 independent binary regressions). Backend DLLs/kernels
//--- (WarriorCPU.dll/WarriorDML.dll/Network.cl) still only ever compute the raw, unweighted
//--- per-neuron delta; this correction - like the sampleWeight scaling below - is applied entirely
//--- on the MQL5 side, so none of the 3 compute backends need to change.
feat: S2 meta-labeling head - binary trade-quality model over the classic-candidate corpus
The NN now has a target that is not per-bar direction (closed, best-of-999
p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of
cost). One net for all 52 pattern-sides, AIType=AI_META.
- NetForward.mqh: the host-side softmax+CE gradient generalized total==3 ->
2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no
compute backend changes.
- SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on
disk (decoupled from the config fingerprint that burned four S1 runs); the
GMT->server offset is measured PER ROW against entryPrice vs bar open
(DST-immune, histogram logged); a window-span regime filter drops the
pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the
input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR).
- Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate
calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell
lets checkpoint selection, the edge floor, the plateau ladder and the
family-wise deploy gate run UNCHANGED: precision reads as win rate among
traded candidates, chance as the base win rate, recalls as sensitivity/
specificity. Era-end META line: coverage x (p - break-even) vs the null.
- Labels are the side-conditional triple-barrier win caches - never the DB's
stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base
rate). Live inference + online learning guarded off until S3.
- Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename
slot keep meta models fully separate from direction models.
Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with
AIType=AI_META; S3 wires the votes via the per-side hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 06:52:31 -04:00
//--- total == 2 is the binary meta-labeling head - a 2-class softmax+CE is identical to a
//--- logistic/BCE head, so it shares this branch; see the matching CNet::backProp() comment.
if ( total = = 3 | | total = = 2 )
2026-08-01 11:27:28 -04:00
{
//--- Logit adjustment (see SetLogitAdjustment): tau*log(prior_c) per class, added to the logit
//--- before the softmax. Backward pass ONLY - the forward pass and every inference path stay
//--- untouched, which is the whole point: the network learns to absorb the offset, so at
//--- inference its RAW argmax is already the balanced-error-optimal decision.
double logit [ 3 ] ;
double maxLogit = - DBL_MAX ;
feat: S2 meta-labeling head - binary trade-quality model over the classic-candidate corpus
The NN now has a target that is not per-bar direction (closed, best-of-999
p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of
cost). One net for all 52 pattern-sides, AIType=AI_META.
- NetForward.mqh: the host-side softmax+CE gradient generalized total==3 ->
2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no
compute backend changes.
- SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on
disk (decoupled from the config fingerprint that burned four S1 runs); the
GMT->server offset is measured PER ROW against entryPrice vs bar open
(DST-immune, histogram logged); a window-span regime filter drops the
pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the
input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR).
- Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate
calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell
lets checkpoint selection, the edge floor, the plateau ladder and the
family-wise deploy gate run UNCHANGED: precision reads as win rate among
traded candidates, chance as the base win rate, recalls as sensitivity/
specificity. Era-end META line: coverage x (p - break-even) vs the null.
- Labels are the side-conditional triple-barrier win caches - never the DB's
stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base
rate). Live inference + online learning guarded off until S3.
- Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename
slot keep meta models fully separate from direction models.
Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with
AIType=AI_META; S3 wires the votes via the per-side hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 06:52:31 -04:00
for ( int n = 0 ; n < total ; n + + )
2026-08-01 11:27:28 -04:00
{
logit [ n ] = CLASS_LOGIT_SCALE * result [ n ] + ( bLogitAdjust ? dLogitAdjust [ n ] : 0.0 ) ;
maxLogit = MathMax ( maxLogit , logit [ n ] ) ;
}
double smax [ 3 ] ;
double sm = 0.0 ;
feat: S2 meta-labeling head - binary trade-quality model over the classic-candidate corpus
The NN now has a target that is not per-bar direction (closed, best-of-999
p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of
cost). One net for all 52 pattern-sides, AIType=AI_META.
- NetForward.mqh: the host-side softmax+CE gradient generalized total==3 ->
2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no
compute backend changes.
- SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on
disk (decoupled from the config fingerprint that burned four S1 runs); the
GMT->server offset is measured PER ROW against entryPrice vs bar open
(DST-immune, histogram logged); a window-span regime filter drops the
pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the
input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR).
- Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate
calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell
lets checkpoint selection, the edge floor, the plateau ladder and the
family-wise deploy gate run UNCHANGED: precision reads as win rate among
traded candidates, chance as the base win rate, recalls as sensitivity/
specificity. Era-end META line: coverage x (p - break-even) vs the null.
- Labels are the side-conditional triple-barrier win caches - never the DB's
stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base
rate). Live inference + online learning guarded off until S3.
- Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename
slot keep meta models fully separate from direction models.
Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with
AIType=AI_META; S3 wires the votes via the per-side hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 06:52:31 -04:00
for ( int n = 0 ; n < total ; n + + )
2026-08-01 11:27:28 -04:00
{
smax [ n ] = exp ( logit [ n ] - maxLogit ) ;
sm + = smax [ n ] ;
}
feat: S2 meta-labeling head - binary trade-quality model over the classic-candidate corpus
The NN now has a target that is not per-bar direction (closed, best-of-999
p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of
cost). One net for all 52 pattern-sides, AIType=AI_META.
- NetForward.mqh: the host-side softmax+CE gradient generalized total==3 ->
2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no
compute backend changes.
- SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on
disk (decoupled from the config fingerprint that burned four S1 runs); the
GMT->server offset is measured PER ROW against entryPrice vs bar open
(DST-immune, histogram logged); a window-span regime filter drops the
pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the
input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR).
- Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate
calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell
lets checkpoint selection, the edge floor, the plateau ladder and the
family-wise deploy gate run UNCHANGED: precision reads as win rate among
traded candidates, chance as the base win rate, recalls as sensitivity/
specificity. Era-end META line: coverage x (p - break-even) vs the null.
- Labels are the side-conditional triple-barrier win caches - never the DB's
stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base
rate). Live inference + online learning guarded off until S3.
- Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename
slot keep meta models fully separate from direction models.
Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with
AIType=AI_META; S3 wires the votes via the per-side hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 06:52:31 -04:00
double gradOverwrite [ ] ;
ArrayResize ( gradOverwrite , total ) ;
for ( int n = 0 ; n < total ; n + + )
2026-08-01 11:27:28 -04:00
{
smax [ n ] / = sm ;
double target = targetVals . At ( n ) ;
double clampedTarget = ( target > 1 ? 1 : target < -1 ? -1 : target ) ;
gradOverwrite [ n ] = clampedTarget - smax [ n ] ;
}
neuron . setGradient ( gradOverwrite ) ;
}
//--- inverse-class-frequency loss weighting (see ExpertSignalAIBase.mqh's Train() for how
//--- sampleWeight is derived). CalcOutputGradient() above only computes the raw, unweighted delta
//--- (WarriorCPU.dll/WarriorDML.dll/Network.cl have no notion of per-sample weighting), so the
//--- gradient buffer is read back, scaled here in MQL5, and pushed back before the hidden layers
//--- below read it via CalcHiddenGradient/sumDOW - avoids touching any of the 3 compute backends.
if ( sampleWeight ! = 1.0 )
{
double gradVals [ ] ;
int gradCount = neuron . getGradient ( gradVals ) ;
if ( gradCount > 0 )
{
for ( int g = 0 ; g < gradCount ; g + + )
gradVals [ g ] * = sampleWeight ;
neuron . setGradient ( gradVals ) ;
}
}
//--- Calc Hidden Gradients
CObject * temp = NULL ;
total = layers . Total ( ) ;
for ( int layerNum = total - 2 ; layerNum > 0 ; layerNum - - )
{
CLayer * nextLayer = currentLayer ;
currentLayer = layers . At ( layerNum ) ;
neuron = currentLayer . At ( 0 ) ;
neuron . calcHiddenGradients ( nextLayer . At ( 0 ) ) ;
}
//--- Layer-1 LSTM special case. The loop above deliberately stops at layerNum > 0 because the INPUT
//--- layer needs no gradient of its own - true for every layer type whose updateInputWeights() derives
//--- its own weight deltas from (own gradient x previous output) inside the kernel: dense
//--- (UpdateWeightsMomentum/Adam) and conv (UpdateWeightsConvMomentum/Adam) both do.
//--- CNeuronLSTMOCL is the ONE exception: LSTM_UpdateWeightsMomentum/Adam do not derive anything, they
//--- only CONSUME the WeightsGradient buffer, and that buffer is filled purely as a SIDE EFFECT of
//--- CNeuronLSTMOCL::calcInputGradients() - which is invoked by the layer BELOW, via its
//--- calcHiddenGradients(target=thisLSTM) dispatch. So an LSTM sitting at layer index 1 (the LSTM_2L
//--- preset: input -> LSTM -> dense -> dense -> output) never gets calcInputGradients() called at all:
//--- WeightsGradient stays at its BufferInit(total, 0) zeros forever, Adam's mt/vt therefore stay 0 and
//--- every weight delta is exactly 0. The LSTM silently never trains - it stays a frozen random
//--- recurrent projection while only the dense taper above it learns, which shows up as ~0% Buy/Sell
//--- recall and a raw-output range that barely moves off its initialisation.
//--- HYBRID_2L (input -> conv -> pool -> LSTM -> dense -> dense -> output) is unaffected: its LSTM is at
//--- index 3, so the pool layer below it makes the call in the normal course of the loop.
//--- Done here rather than by relaxing the loop bound so the loop's currentLayer/nextLayer bookkeeping
//--- is untouched, and so no other topology pays for an extra kernel dispatch it does not need.
if ( total > = 3 )
{
CLayer * firstHidden = layers . At ( 1 ) ;
CLayer * inputLayer = layers . At ( 0 ) ;
if ( CheckPointer ( firstHidden ) ! = POINTER_INVALID & & CheckPointer ( inputLayer ) ! = POINTER_INVALID & &
CheckPointer ( firstHidden . At ( 0 ) ) ! = POINTER_INVALID & & CheckPointer ( inputLayer . At ( 0 ) ) ! = POINTER_INVALID & &
firstHidden . At ( 0 ) . Type ( ) = = defNeuronLSTMOCL )
{
CNeuronLSTMOCL * lstm = firstHidden . At ( 0 ) ;
CNeuronBaseOCL * inputNeuron = inputLayer . At ( 0 ) ;
//--- Safe to run now and only now: the loop above has already filled this LSTM's own Gradient
//--- (it processed layerNum == 1), which is exactly what LSTMGateGradient reads.
if ( ! lstm . calcInputGradients ( inputNeuron ) )
printf ( " %s: LSTM at layer 1 failed to compute its weight gradients - it will not train this step " , __FUNCTION__ ) ;
}
}
//---
feat: mini-batch gradient accumulation (F4), front-end-aware capacity budget (F6), split Wyckoff categoricals (N1)
Completes the 2026-08-09 training audit. FORCES A RETRAIN of every
Wyckoff-enabled config (N1 re-keys the fingerprint), and BOTH DLLs must be
redeployed alongside the .ex5 - they carry new exports.
F4 - mini-batch accumulation, TRAIN_BATCH_SIZE=32. Training was pure online
SGD (one weight update per bar), which is the mechanical source of the
era-to-era whipsaw every downstream guard was built to cope with. The O(n^2)
outer product is native - AccumulateWeightGrad / AccumulateWeightGradConv /
AccumulateBufferInto in Network.cl, WarriorCPU and WarriorDML - while the
optimizer step is host-side MQL5 shared by all tiers (ApplyAccumToBlock), so
there is one Adam/SGD implementation instead of four that can drift.
- the LSTM needs no outer-product kernel (WeightsGradient already holds the
sample's full dW) but could NOT simply be left un-zeroed between samples:
CPU_LSTMSeqBackward/DML_LSTMSeqBackward memset it on entry. Hence a
separate accumulator plus an elementwise add.
- batch-norm gamma/beta accumulate in host arrays, not new BatchOptions
slots - BN_OPT_STRIDE is baked into every persisted .nnw.
- scoped to pass 2; online learning keeps immediate updates. Every save /
checkpoint / scoring boundary flushes, scaling by the real sample count.
- degrades to per-sample updates (one log line) on a tier that cannot
accumulate, so old devices and DLL-free builds are unaffected.
- verified offline: DirectML/batch_accum_check.cpp drives the real exports
against an independent reference; at B=1 the accumulator matches the
shipped unbatched kernel's own gradient to 1.1e-16. Math only - the
in-situ check remains the per-layer dW/W report on a real era.
F6 - ComputeFirstLayerWidth budgeted against the RAW input width even where a
conv/LSTM front end had already reduced it, so an LSTM's dense stack was
charged for 1,280 inputs when it receives 64. Confirmed from the deployed
.cfg files: CONV, LSTM and HYBRID were all pinned at the 16-unit floor. Now
budgeted against the front-end output and capped at it (never fan out), with
the derivation reordered so both stages settle first.
N1 - EventCode/EventPhase/StructuralPhase are signed categoricals packing
direction and Wyckoff stage into one scalar across a sign discontinuity. Split
into direction + [0,1] magnitude, the same convention the base OHLC block uses.
Information-preserving; 13 readings now occupy 16 inputs.
Compiled clean (0 errors, 0 warnings); both DLLs rebuilt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 11:48:03 -04:00
//--- WEIGHT UPDATE. Two shapes, selected by the effective batch size (2026-08-09 audit, F4).
//--- Per-sample (size 1) is the original path, untouched. Batched accumulates this sample's
//--- gradients into each layer's accumulator and takes ONE optimizer step every `batch` samples,
//--- which is what turns a maximally-noisy online update into an averaged one.
int batch = BatchSize ( ) ;
2026-08-01 11:27:28 -04:00
CLayer * prevLayer = layers . At ( total - 1 ) ;
feat: mini-batch gradient accumulation (F4), front-end-aware capacity budget (F6), split Wyckoff categoricals (N1)
Completes the 2026-08-09 training audit. FORCES A RETRAIN of every
Wyckoff-enabled config (N1 re-keys the fingerprint), and BOTH DLLs must be
redeployed alongside the .ex5 - they carry new exports.
F4 - mini-batch accumulation, TRAIN_BATCH_SIZE=32. Training was pure online
SGD (one weight update per bar), which is the mechanical source of the
era-to-era whipsaw every downstream guard was built to cope with. The O(n^2)
outer product is native - AccumulateWeightGrad / AccumulateWeightGradConv /
AccumulateBufferInto in Network.cl, WarriorCPU and WarriorDML - while the
optimizer step is host-side MQL5 shared by all tiers (ApplyAccumToBlock), so
there is one Adam/SGD implementation instead of four that can drift.
- the LSTM needs no outer-product kernel (WeightsGradient already holds the
sample's full dW) but could NOT simply be left un-zeroed between samples:
CPU_LSTMSeqBackward/DML_LSTMSeqBackward memset it on entry. Hence a
separate accumulator plus an elementwise add.
- batch-norm gamma/beta accumulate in host arrays, not new BatchOptions
slots - BN_OPT_STRIDE is baked into every persisted .nnw.
- scoped to pass 2; online learning keeps immediate updates. Every save /
checkpoint / scoring boundary flushes, scaling by the real sample count.
- degrades to per-sample updates (one log line) on a tier that cannot
accumulate, so old devices and DLL-free builds are unaffected.
- verified offline: DirectML/batch_accum_check.cpp drives the real exports
against an independent reference; at B=1 the accumulator matches the
shipped unbatched kernel's own gradient to 1.1e-16. Math only - the
in-situ check remains the per-layer dW/W report on a real era.
F6 - ComputeFirstLayerWidth budgeted against the RAW input width even where a
conv/LSTM front end had already reduced it, so an LSTM's dense stack was
charged for 1,280 inputs when it receives 64. Confirmed from the deployed
.cfg files: CONV, LSTM and HYBRID were all pinned at the 16-unit floor. Now
budgeted against the front-end output and capped at it (never fan out), with
the derivation reordered so both stages settle first.
N1 - EventCode/EventPhase/StructuralPhase are signed categoricals packing
direction and Wyckoff stage into one scalar across a sign discontinuity. Split
into direction + [0,1] magnitude, the same convention the base OHLC block uses.
Information-preserving; 13 readings now occupy 16 inputs.
Compiled clean (0 errors, 0 warnings); both DLLs rebuilt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 11:48:03 -04:00
if ( batch < = 1 )
{
for ( int layerNum = total - 1 ; layerNum > 0 ; layerNum - - )
{
currentLayer = prevLayer ;
prevLayer = layers . At ( layerNum - 1 ) ;
neuron = currentLayer . At ( 0 ) ;
neuron . updateInputWeights ( prevLayer . At ( 0 ) ) ;
}
return ;
}
if ( ! m_batchBegun & & ! BeginBatch ( ) )
return ;
2026-08-01 11:27:28 -04:00
for ( int layerNum = total - 1 ; layerNum > 0 ; layerNum - - )
{
currentLayer = prevLayer ;
prevLayer = layers . At ( layerNum - 1 ) ;
neuron = currentLayer . At ( 0 ) ;
feat: mini-batch gradient accumulation (F4), front-end-aware capacity budget (F6), split Wyckoff categoricals (N1)
Completes the 2026-08-09 training audit. FORCES A RETRAIN of every
Wyckoff-enabled config (N1 re-keys the fingerprint), and BOTH DLLs must be
redeployed alongside the .ex5 - they carry new exports.
F4 - mini-batch accumulation, TRAIN_BATCH_SIZE=32. Training was pure online
SGD (one weight update per bar), which is the mechanical source of the
era-to-era whipsaw every downstream guard was built to cope with. The O(n^2)
outer product is native - AccumulateWeightGrad / AccumulateWeightGradConv /
AccumulateBufferInto in Network.cl, WarriorCPU and WarriorDML - while the
optimizer step is host-side MQL5 shared by all tiers (ApplyAccumToBlock), so
there is one Adam/SGD implementation instead of four that can drift.
- the LSTM needs no outer-product kernel (WeightsGradient already holds the
sample's full dW) but could NOT simply be left un-zeroed between samples:
CPU_LSTMSeqBackward/DML_LSTMSeqBackward memset it on entry. Hence a
separate accumulator plus an elementwise add.
- batch-norm gamma/beta accumulate in host arrays, not new BatchOptions
slots - BN_OPT_STRIDE is baked into every persisted .nnw.
- scoped to pass 2; online learning keeps immediate updates. Every save /
checkpoint / scoring boundary flushes, scaling by the real sample count.
- degrades to per-sample updates (one log line) on a tier that cannot
accumulate, so old devices and DLL-free builds are unaffected.
- verified offline: DirectML/batch_accum_check.cpp drives the real exports
against an independent reference; at B=1 the accumulator matches the
shipped unbatched kernel's own gradient to 1.1e-16. Math only - the
in-situ check remains the per-layer dW/W report on a real era.
F6 - ComputeFirstLayerWidth budgeted against the RAW input width even where a
conv/LSTM front end had already reduced it, so an LSTM's dense stack was
charged for 1,280 inputs when it receives 64. Confirmed from the deployed
.cfg files: CONV, LSTM and HYBRID were all pinned at the 16-unit floor. Now
budgeted against the front-end output and capped at it (never fan out), with
the derivation reordered so both stages settle first.
N1 - EventCode/EventPhase/StructuralPhase are signed categoricals packing
direction and Wyckoff stage into one scalar across a sign discontinuity. Split
into direction + [0,1] magnitude, the same convention the base OHLC block uses.
Information-preserving; 13 readings now occupy 16 inputs.
Compiled clean (0 errors, 0 warnings); both DLLs rebuilt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 11:48:03 -04:00
neuron . accumulateInputWeightGrads ( prevLayer . At ( 0 ) ) ;
2026-08-01 11:27:28 -04:00
}
feat: mini-batch gradient accumulation (F4), front-end-aware capacity budget (F6), split Wyckoff categoricals (N1)
Completes the 2026-08-09 training audit. FORCES A RETRAIN of every
Wyckoff-enabled config (N1 re-keys the fingerprint), and BOTH DLLs must be
redeployed alongside the .ex5 - they carry new exports.
F4 - mini-batch accumulation, TRAIN_BATCH_SIZE=32. Training was pure online
SGD (one weight update per bar), which is the mechanical source of the
era-to-era whipsaw every downstream guard was built to cope with. The O(n^2)
outer product is native - AccumulateWeightGrad / AccumulateWeightGradConv /
AccumulateBufferInto in Network.cl, WarriorCPU and WarriorDML - while the
optimizer step is host-side MQL5 shared by all tiers (ApplyAccumToBlock), so
there is one Adam/SGD implementation instead of four that can drift.
- the LSTM needs no outer-product kernel (WeightsGradient already holds the
sample's full dW) but could NOT simply be left un-zeroed between samples:
CPU_LSTMSeqBackward/DML_LSTMSeqBackward memset it on entry. Hence a
separate accumulator plus an elementwise add.
- batch-norm gamma/beta accumulate in host arrays, not new BatchOptions
slots - BN_OPT_STRIDE is baked into every persisted .nnw.
- scoped to pass 2; online learning keeps immediate updates. Every save /
checkpoint / scoring boundary flushes, scaling by the real sample count.
- degrades to per-sample updates (one log line) on a tier that cannot
accumulate, so old devices and DLL-free builds are unaffected.
- verified offline: DirectML/batch_accum_check.cpp drives the real exports
against an independent reference; at B=1 the accumulator matches the
shipped unbatched kernel's own gradient to 1.1e-16. Math only - the
in-situ check remains the per-layer dW/W report on a real era.
F6 - ComputeFirstLayerWidth budgeted against the RAW input width even where a
conv/LSTM front end had already reduced it, so an LSTM's dense stack was
charged for 1,280 inputs when it receives 64. Confirmed from the deployed
.cfg files: CONV, LSTM and HYBRID were all pinned at the 16-unit floor. Now
budgeted against the front-end output and capped at it (never fan out), with
the derivation reordered so both stages settle first.
N1 - EventCode/EventPhase/StructuralPhase are signed categoricals packing
direction and Wyckoff stage into one scalar across a sign discontinuity. Split
into direction + [0,1] magnitude, the same convention the base OHLC block uses.
Information-preserving; 13 readings now occupy 16 inputs.
Compiled clean (0 errors, 0 warnings); both DLLs rebuilt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 11:48:03 -04:00
//--- No separate LSTM sweep here, deliberately: the loop above already visits EVERY layer down to
//--- index 1, and each layer's accumulate covers exactly the block its own updateInputWeights would
//--- have written (dense/batch-norm: the block held by the layer below; conv and LSTM: their own).
//--- The layer-1 LSTM asymmetry handled further up applies to calcInputGradients, which FILLS
//--- WeightsGradient - not to this pass, which only reads it.
m_batchCount + + ;
if ( m_batchCount > = batch )
FlushBatch ( ) ;
2026-08-01 11:27:28 -04:00
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CNet : : getResults ( CArrayDouble * & resultVals )
{
if ( CheckPointer ( resultVals ) = = POINTER_INVALID )
{
resultVals = new CArrayDouble ( ) ;
if ( CheckPointer ( resultVals ) = = POINTER_INVALID )
return ;
}
//---
resultVals . Clear ( ) ;
if ( CheckPointer ( layers ) = = POINTER_INVALID | | layers . Total ( ) < = 0 )
return ;
//---
CLayer * output = layers . At ( layers . Total ( ) - 1 ) ;
if ( CheckPointer ( output ) = = POINTER_INVALID )
return ;
//---
if ( CheckPointer ( opencl ) ! = POINTER_INVALID | | CheckPointer ( directml ) ! = POINTER_INVALID )
{
switch ( output . At ( 0 ) . Type ( ) )
{
case defNeuronBaseOCL :
case defNeuronConvOCL :
case defNeuronPoolOCL :
case defNeuronLSTMOCL :
{
CNeuronBaseOCL * temp = output . At ( 0 ) ;
temp . getOutputVal ( resultVals ) ;
return ;
}
}
}
//--- pure-MQL5 inference: OCL output neuron computed host-side, read without a device BufferRead.
if ( m_cpuInference )
{
switch ( output . At ( 0 ) . Type ( ) )
{
case defNeuronBaseOCL :
case defNeuronConvOCL :
case defNeuronPoolOCL :
case defNeuronLSTMOCL :
{
CNeuronBaseOCL * temp = output . At ( 0 ) ;
temp . GetOutputsCPU ( resultVals ) ;
return ;
}
}
}
CNeuronBase * neuron = NULL ;
CLayer * temp = NULL ;
int total = output . Total ( ) ;
if ( output . At ( 0 ) . Type ( ) = = defNeuron )
total - - ;
//---
for ( int i = 0 ; i < total ; i + + )
{
CObject * obj = output . At ( i ) ;
if ( CheckPointer ( obj ) = = POINTER_INVALID )
continue ;
if ( obj . Type ( ) = = defNeuron )
{
neuron = ( CNeuronBase * ) obj ;
resultVals . Add ( neuron . getOutputVal ( ) ) ;
continue ;
}
if ( obj . Type ( ) = = defNeuronPool )
{
CNeuronPool * n = ( CNeuronPool * ) obj ;
temp = n . getOutputLayer ( ) ;
for ( int ii = 0 ; ii < temp . Total ( ) ; ii + + )
{
CObject * poolObj = temp . At ( ii ) ;
if ( CheckPointer ( poolObj ) = = POINTER_INVALID )
continue ;
CNeuronBase * poolNeuron = ( CNeuronBase * ) poolObj ;
resultVals . Add ( poolNeuron . getOutputVal ( ) ) ;
}
}
}
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Install the per-class logit offsets - see the declaration. |
//| Expects tau*log(prior_c) already computed by the caller, which is |
//| the only place that knows the training set's class distribution. |
//+------------------------------------------------------------------+
void CNet : : SetLogitAdjustment ( const double & offsets [ ] )
{
if ( ArraySize ( offsets ) < 3 )
{
//--- Refuse rather than half-apply: a partially filled offset vector would silently bias two
//--- classes against a third, which is far worse than running unadjusted.
bLogitAdjust = false ;
return ;
}
for ( int i = 0 ; i < 3 ; i + + )
{
//--- Guard against a non-finite offset reaching the softmax (a zero prior would give -inf and
//--- turn every gradient into NaN). A class with no examples at all simply gets no push.
double v = offsets [ i ] ;
if ( ! MathIsValidNumber ( v ) )
v = 0.0 ;
dLogitAdjust [ i ] = v ;
}
bLogitAdjust = true ;
}
# endif