feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Warrior_EA |
|
|
|
|
|
//| AnimateDread |
|
|
|
|
|
//| |
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
//| CBaselineComparator - two non-NN learners on the NN's matrix. |
|
|
|
|
|
//| |
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
//+------------------------------------------------------------------+
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
#ifndef WARRIOR_TRAINING_BASELINECOMPARATOR_MQH
|
|
|
|
|
#define WARRIOR_TRAINING_BASELINECOMPARATOR_MQH
|
|
|
|
|
#include "ITrainingData.mqh"
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
|
2026-08-20 14:04:06 -04:00
|
|
|
//--- ONCE-PER-CHART, same doctrine as g_ensembleChartMiReportDone (AutoTune.mqh) and for a stronger
|
2026-08-22 00:25:52 -04:00
|
|
|
//--- reason. Four un-chunked forest+MLP+OLS fits over a ~1000-column, 4000-row design, to print the
|
|
|
|
|
//--- same numbers four times.
|
2026-08-20 14:04:06 -04:00
|
|
|
bool g_ensembleChartBaselinesDone = false;
|
|
|
|
|
|
fix(baselines): the diagnostic held the EA thread long enough for MetaTrader to kill the process
Run_Alglib_Baselines was described as bounded because it 'cannot outrun the bar it runs on'. That
bound is four hours on H4, it was never enforced, and it is the wrong target: the EA is single
threaded, so while this pass runs there is no training, no panel, no tick handling, and a removal
request only queues.
Measured today on SP500 H4, 800 inputs x 4000 rows: forest 5 s, MLP 52 s, then the 3-fold CV -
three more full retrains. At 15:18:16, 83 s into it, MetaTrader force-terminated the EA. OnDeinit
never ran, so every arrow, panel object and label was orphaned on the chart. The init purge cleans
that up on re-attach; nothing in the pass writes to a cache, saves a model or feeds a decision, so
the damage was the freeze and the dirty chart, not corrupted state.
Three changes:
- BASELINE_MAX_CELLS. Every fit costs O(rows x width) and the row caps carried no width term.
Rows now shrink as the window widens - 1000 instead of 4000 at 800 columns - and the log says
when the cap bit.
- BASELINE_BUDGET_MS with BaselineBudgetSpent(), replacing the bare ShutdownRequested() calls at
each phase boundary. A stop and a spent budget need the same answer and only one was asked.
- The cross-validation is now declined BEFORE it starts, from its predicted cost - the measured MLP
time times the fold count. A boundary check cannot help once a phase is in flight, which is
exactly the phase that was in flight when the process died.
Also: the MLP now reports its weight count against its row count. At 800 inputs it carries ~6,400
weights, and today's run duly printed a training class error of 0.000. That is knowable from the
shape before fitting, so it is stated rather than left to a cross-validation that may not run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:22:42 -04:00
|
|
|
//--- THE BOUND IS THE EA THREAD, NOT THE BAR. This pass used to be described as bounded because it
|
|
|
|
|
//--- "cannot outrun the bar it runs on" - four hours on H4. That is the wrong target and was never
|
2026-08-22 00:25:52 -04:00
|
|
|
//--- enforced: the EA is single threaded, so for as long as this runs there is no training, no
|
|
|
|
|
//--- panel, no tick handling, and a removal request just queues.
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
#define BASELINE_MAX_TRAIN_ROWS 4000
|
|
|
|
|
#define BASELINE_MAX_SCORE_ROWS 4000
|
fix(baselines): the diagnostic held the EA thread long enough for MetaTrader to kill the process
Run_Alglib_Baselines was described as bounded because it 'cannot outrun the bar it runs on'. That
bound is four hours on H4, it was never enforced, and it is the wrong target: the EA is single
threaded, so while this pass runs there is no training, no panel, no tick handling, and a removal
request only queues.
Measured today on SP500 H4, 800 inputs x 4000 rows: forest 5 s, MLP 52 s, then the 3-fold CV -
three more full retrains. At 15:18:16, 83 s into it, MetaTrader force-terminated the EA. OnDeinit
never ran, so every arrow, panel object and label was orphaned on the chart. The init purge cleans
that up on re-attach; nothing in the pass writes to a cache, saves a model or feeds a decision, so
the damage was the freeze and the dirty chart, not corrupted state.
Three changes:
- BASELINE_MAX_CELLS. Every fit costs O(rows x width) and the row caps carried no width term.
Rows now shrink as the window widens - 1000 instead of 4000 at 800 columns - and the log says
when the cap bit.
- BASELINE_BUDGET_MS with BaselineBudgetSpent(), replacing the bare ShutdownRequested() calls at
each phase boundary. A stop and a spent budget need the same answer and only one was asked.
- The cross-validation is now declined BEFORE it starts, from its predicted cost - the measured MLP
time times the fold count. A boundary check cannot help once a phase is in flight, which is
exactly the phase that was in flight when the process died.
Also: the MLP now reports its weight count against its row count. At 800 inputs it carries ~6,400
weights, and today's run duly printed a training class error of 0.000. That is knowable from the
shape before fitting, so it is stated rather than left to a cross-validation that may not run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:22:42 -04:00
|
|
|
//--- ROWS x COLUMNS, because every fit here costs O(rows x width) and the row caps above carry no
|
|
|
|
|
//--- width term. 800k = the 4000-row cap at the ~200-column window these numbers were chosen for;
|
|
|
|
|
//--- at this build's 800 columns it buys 1000 rows and an MLP fit of ~13 s instead of 52 s.
|
|
|
|
|
#define BASELINE_MAX_CELLS 800000
|
|
|
|
|
//--- Wall clock for the whole pass, checked between phases. Deliberately far below the ~4,500 ms
|
|
|
|
|
//--- OnDeinit budget's timescale being reached at all: the point is that a stop never has to wait.
|
|
|
|
|
#define BASELINE_BUDGET_MS 45000
|
2026-08-21 18:12:14 -04:00
|
|
|
//--- CALIBRATION POINT for the linear phase, which is the one that cannot be bounded by a boundary
|
2026-08-22 00:25:52 -04:00
|
|
|
//--- check. LRBuild solves a (width+1)^2 normal-equation system, so its cost grows as width^3 and
|
|
|
|
|
//--- does not care how few rows there are.
|
2026-08-21 18:12:14 -04:00
|
|
|
#define BASELINE_LR_MS_AT_800 535000.0
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
#define BASELINE_TREES 32
|
|
|
|
|
#define BASELINE_SUBSAMPLE_RATIO 0.66
|
feat(baselines): ALGLIB's MLP as a third baseline, not a replacement
Asked whether ALGLIB's perceptron should replace ours "only if it is
better". This measures that instead of assuming it, and the measurement
is worth more than the swap would have been.
The forest and the linear fit test whether the MATRIX carries direction.
Neither tests whether OUR CODE is what fails. ALGLIB's MLP does: it is a
long-standing independent implementation of roughly what our own dense
stack computes, trained on the identical rows through the identical
gate. That distinction is not hypothetical here - two silent
implementation faults have already invalidated every model-based
negative behind them (a transposed dense hidden gradient, so backprop
was never backprop; and Adam storing sqrt(v) and feeding it back as
variance, so every "Adam" run was SGD). If alglib-mlp clears a bar our
own net cannot on the same rows, the fault is in our code.
Trained BEFORE the target column is rewritten for the linear fit:
MLPCreateC1 builds a softmax classifier whose trainer wants the class
index, which is what the matrix still holds at that point. Scored by
argmax with Neutral abstaining, exactly as the forest is. Sidak family
is now 3, not 2.
Deliberately NOT a replacement, and the gap is not close: ALGLIB's MLP
is feed-forward only (no conv, no recurrence - so it could not stand in
for three of the four members at all), and it has no batch norm, no
logit-adjusted loss for the class imbalance, no era resumption, no
OpenCL or CPU-DLL backend and no .nnw. Adopting it would trade a
maintained architecture for a control.
Kept small on purpose - 8 hidden units, 1 restart, 100 iterations, decay
at the 0.001 the ALGLIB docs recommend when you have no reason to pick
another. A wide net would spend the six-core box's hours answering a
question about capacity when the question is about implementation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:19:53 -04:00
|
|
|
//--- Three models are tried, so the family-wise correction is over three - the same rule the deploy
|
|
|
|
|
//--- gate applies to its own best-of-N selection. Reporting each model's uncorrected p as if it were
|
|
|
|
|
//--- the only one tried is precisely the error catalogued four times already.
|
|
|
|
|
#define BASELINE_MODELS_TRIED 3
|
2026-08-22 00:25:52 -04:00
|
|
|
//--- MLP baseline. Small and bounded on purpose: this is a CONTROL, not a competitor. One restart,
|
|
|
|
|
//--- a hard iteration cap, and the decay ALGLIB's own docs recommend when you have no reason to
|
|
|
|
|
//--- pick another.
|
feat(baselines): ALGLIB's MLP as a third baseline, not a replacement
Asked whether ALGLIB's perceptron should replace ours "only if it is
better". This measures that instead of assuming it, and the measurement
is worth more than the swap would have been.
The forest and the linear fit test whether the MATRIX carries direction.
Neither tests whether OUR CODE is what fails. ALGLIB's MLP does: it is a
long-standing independent implementation of roughly what our own dense
stack computes, trained on the identical rows through the identical
gate. That distinction is not hypothetical here - two silent
implementation faults have already invalidated every model-based
negative behind them (a transposed dense hidden gradient, so backprop
was never backprop; and Adam storing sqrt(v) and feeding it back as
variance, so every "Adam" run was SGD). If alglib-mlp clears a bar our
own net cannot on the same rows, the fault is in our code.
Trained BEFORE the target column is rewritten for the linear fit:
MLPCreateC1 builds a softmax classifier whose trainer wants the class
index, which is what the matrix still holds at that point. Scored by
argmax with Neutral abstaining, exactly as the forest is. Sidak family
is now 3, not 2.
Deliberately NOT a replacement, and the gap is not close: ALGLIB's MLP
is feed-forward only (no conv, no recurrence - so it could not stand in
for three of the four members at all), and it has no batch norm, no
logit-adjusted loss for the class imbalance, no era resumption, no
OpenCL or CPU-DLL backend and no .nnw. Adopting it would trade a
maintained architecture for a control.
Kept small on purpose - 8 hidden units, 1 restart, 100 iterations, decay
at the 0.001 the ALGLIB docs recommend when you have no reason to pick
another. A wide net would spend the six-core box's hours answering a
question about capacity when the question is about implementation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:19:53 -04:00
|
|
|
#define BASELINE_MLP_HIDDEN 8
|
|
|
|
|
#define BASELINE_MLP_DECAY 0.001
|
|
|
|
|
#define BASELINE_MLP_RESTARTS 1
|
|
|
|
|
#define BASELINE_MLP_WSTEP 0.01
|
|
|
|
|
#define BASELINE_MLP_MAXITS 100
|
2026-08-22 00:25:52 -04:00
|
|
|
//--- Folds for the MLP's cross-validated error bar. Three, not the usual five or ten: each fold is
|
|
|
|
|
//--- a full retrain, so this multiplies the MLP's cost by exactly this number on a six-core 2013
|
|
|
|
|
//--- box.
|
feat(baselines): combining-weight fit, MLP cross-validation, all-lags correlation
Three ALGLIB additions, all measurement-only and all under the existing
Run_Alglib_Baselines switch.
MinBLEIC COMBINING WEIGHTS. The live ensemble weights each member by its
own pooled holdout win rate - a defensible prior, but not a fit, and
nothing has ever asked what mixture minimises error on the bars the
members disagreed about. Two individually-mediocre members wrong in
different places can beat one individually better, and a per-member win
rate cannot express that because it never looks at them jointly.
Solved on the simplex (w >= 0, sum w = 1), which is exactly what
MinBLEIC is for. Non-negative because a negative weight asserts "trade
the opposite of this member", a claim ~60 effective observations cannot
support. Least squares on the signed outcome rather than precision:
precision is a STEP function of the threshold that no gradient method
can walk, and optimising a smooth proxy for a step decision is how
c3daded put every operating point 14pp underwater - so the result is
reported in BOTH currencies, the SSE it minimised and the directional
hit rate the mixture would actually have scored against the equal mix.
If the second does not improve, the first is noise.
This needed data that did not exist: g_ensVoteSum accumulates member
contributions and the sum destroys the decomposition, while
g_ensVoteVoterMask records only WHETHER a member voted, never what.
g_ensVoteMember[] keeps them unsummed. The live arithmetic is untouched.
ENS_MAX_MEMBERS is 8 and deliberately larger than MAX_AI_SIGNALS (5):
independent caps, over-allocating is free, and matching them would make
this array silently short the day the registry grows - a cap that has
already dropped a member once without saying so.
MLPKFoldCVLBFGS. Every baseline row carries a binomial SE, which is the
sampling error of SCORING a fixed model and says nothing about how much
the FIT moves. One LBFGS run from one random start can land anywhere,
and a baseline that cleared or missed the bar on luck of initialisation
reads exactly like one that did it on merit. 3 folds, because each is a
full retrain. LBFGS not LM - LM builds a Hessian over ~7,700 weights.
CCorr ALL-LAGS PROFILE. Added BESIDE the MI lag profile, not instead:
MI catches nonlinear dependence and is the stronger negative, which is
why it settled the verdict - but its per-lag permutation null limits it
to ~20 lags. FFT correlation gets every lag in one O(n log n) pass, so
linear structure parked at lag 300 would surface for free. Different
question, not a replacement. Walks CONTIGUOUS bars, unlike everything
else in this file, because a lag index is meaningless otherwise; both
series are mean-centred first since CorrR1D is a raw sum of products;
and the max over columns x lags is judged against a Sidak family of
exactly that size, not a bare 2-sigma line.
fasttransforms.mqh needed its own include - verified that none of
ap/optimization/statistics/solvers/linalg reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:44:36 -04:00
|
|
|
#define BASELINE_MLP_CV_FOLDS 3
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
//--- Below this the fit is not worth reporting: an OOS slice this thin cannot resolve an edge from
|
|
|
|
|
//--- noise at any precision, and a train slice this thin cannot fit anything but its own rows.
|
|
|
|
|
#define BASELINE_MIN_ROWS 200
|
feat(baselines): matrix redundancy and a declustered regime test
Two ALGLIB diagnostics on the pass that already builds the matrix, under
the same Run_Alglib_Baselines switch, so they cost nothing by default
and share one walk of the feature pipeline.
REDUNDANCY (CBaseStat::PearsonCorrM + CPCAnalysis::PCABuildBasis).
The alt-data block already produced this finding once, by hand: 13
features x 16 window slots = 208 inputs spanning ~11.5 effective
dimensions, and the cost was not wasted parameters but a gradient
weighting bias, since batch norm rescales collinear copies without
decorrelating them. This generalises that measurement to every column
instead of the one block someone thought to check.
Run on the ANCHOR BAR'S ROW, not the full window. The window is
m_historyBars near-copies of the same columns at different lags, so
per-bar redundancy is the question worth asking - the lag structure is
what the conv/LSTM stage exists to exploit - and it keeps the eigensolve
at ~60x60 rather than ~960x960.
REGIME STABILITY (CMannWhitneyU::CMannWhitneyUTest) on the net's own
signed margin: its conviction, signed by whether the conviction paid,
older half of the OOS window against newer.
DECLUSTERED FIRST, which is the whole reason this is not a one-line
call. Triple-barrier labels overlap, so consecutive scored bars share
the price action that resolved them and are not independent draws.
Feeding all of them to a rank test yields a p computed on an n it does
not have - the same error label overlap invalidated everywhere else
here. One sample per label horizon costs most of the rows and buys a p
that means something; when too few survive, it says so and declines.
Both report and neither acts. Wiring the regime flag to position sizing
is the obvious next step and the one to take only after watching the
number: run every era on ~60 effective observations, a 0.05 test crosses
by chance regularly, and an auto-derisk on that is a random position
size generator.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:01:34 -04:00
|
|
|
//--- Redundancy report: the share of total variance the leading components must reach before the
|
|
|
|
|
//--- count is called the matrix's effective width, and the |r| above which a pair is called a copy.
|
|
|
|
|
#define BASELINE_PCA_VAR_SHARE 0.95
|
|
|
|
|
#define BASELINE_COLLINEAR_ABS_R 0.90
|
|
|
|
|
//--- A regime test needs at least this many DECLUSTERED samples per half to say anything at all.
|
|
|
|
|
#define BASELINE_MIN_REGIME_N 20
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| PUT TWO COMPLETELY DIFFERENT LEARNERS ON THE NET'S OWN MATRIX. |
|
|
|
|
|
//| |
|
|
|
|
|
//| One question, and the whole module exists to answer it: when the |
|
|
|
|
|
//| net scores at chance, is that the MATRIX or is it us? An Alglib |
|
|
|
|
|
//| random forest and an Alglib MLP fit the same rows, the same |
|
|
|
|
|
//| labels and the same split. All at chance means the data is the |
|
|
|
|
|
//| limit. An off-the-shelf MLP well ABOVE our net on those rows |
|
|
|
|
|
//| would mean the limit is our implementation. |
|
|
|
|
|
//| |
|
|
|
|
|
//| It is a CONTROL, not a competitor - which is why the models are |
|
|
|
|
|
//| small, capped and un-tuned on purpose. |
|
|
|
|
|
//| |
|
|
|
|
|
//| It knows nothing about CExpertSignalAIBase. It reads a |
|
|
|
|
|
//| CTrainingDataView and prints; that is the entire contract, and it |
|
|
|
|
|
//| is what lets this be read, changed or dropped on its own. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
class CBaselineComparator
|
|
|
|
|
{
|
|
|
|
|
private:
|
|
|
|
|
//--- BORROWED. The signal owns both the view and this object.
|
|
|
|
|
CTrainingDataView *m_data;
|
|
|
|
|
//--- ONCE PER MODEL. Set before the first thing that can fail, so a run that bails does not
|
|
|
|
|
//--- retry every era for the rest of the session.
|
|
|
|
|
bool m_done;
|
|
|
|
|
|
|
|
|
|
int BaselineCandidateBars(const int lo, const int hi, const int bars,
|
|
|
|
|
const int cap, int &rows[]);
|
|
|
|
|
bool BaselineBudgetSpent(const uint startTick, const string nextPhase);
|
|
|
|
|
void ReportCombiningWeights(void);
|
|
|
|
|
double CombinerSSE(const double &w[], const int members, const double &s[],
|
|
|
|
|
const double &t[], const int rows);
|
|
|
|
|
void ReportLinearLagProfile(const int bars, const int oosCutoff);
|
|
|
|
|
void ReportFeatureRedundancy(CMatrixDouble &rows, const int n);
|
|
|
|
|
void ReportRegimeStability(double &margins[], int &marginBars[]);
|
|
|
|
|
void ReportBaselineModel(const string label, const int calls, const int hits,
|
|
|
|
|
const int scored, const double chancePct);
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
CBaselineComparator(void) : m_data(NULL), m_done(false) { }
|
|
|
|
|
~CBaselineComparator(void) { m_data = NULL; }
|
|
|
|
|
//--- Hand it the view once, when the owner is constructed. Everything else it needs it asks
|
|
|
|
|
//--- for; there is deliberately no setter for anything else.
|
|
|
|
|
void Bind(CTrainingDataView *data) { m_data = data; }
|
|
|
|
|
//--- Already reported? The owner asks so it can say so rather than silently skipping.
|
|
|
|
|
bool Done(void) const { return m_done; }
|
|
|
|
|
void RunBaselineComparison(const int bars, const int totalIter, const int oosCutoff);
|
|
|
|
|
};
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Bar indices in [lo, hi) that carry a label and could carry a full |
|
|
|
|
|
//| feature window, thinned by a uniform stride to at most `cap`. |
|
|
|
|
|
//| |
|
|
|
|
|
//| The window check is a bounds test only - whether BufferTempData |
|
|
|
|
|
//| will actually produce the row is not known until it is asked, and |
|
|
|
|
|
//| asking is the expensive part. So this over-selects slightly and |
|
|
|
|
|
//| the callers report the count they really got. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
int CBaselineComparator::BaselineCandidateBars(const int lo, const int hi, const int bars,
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
const int cap, int &rows[])
|
|
|
|
|
{
|
|
|
|
|
int all[];
|
|
|
|
|
ArrayResize(all, 0);
|
|
|
|
|
for(int r = MathMax(lo, 0); r < hi; r++)
|
|
|
|
|
{
|
|
|
|
|
//--- The window reaches BACK from r, so once it runs off the deep end every larger r does too.
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
if(r + m_data.HistoryBars() - 1 >= bars)
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
break;
|
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
|
|
|
//--- HasLabel is the whole resolvedness test: the finality-gated cache never holds an
|
|
|
|
|
//--- unresolved bar.
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
if(!m_data.HasLabel(r))
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
continue;
|
|
|
|
|
int n = ArraySize(all);
|
|
|
|
|
ArrayResize(all, n + 1);
|
|
|
|
|
all[n] = r;
|
|
|
|
|
}
|
|
|
|
|
int total = ArraySize(all);
|
|
|
|
|
if(total <= cap || cap <= 0)
|
|
|
|
|
{
|
|
|
|
|
ArrayResize(rows, total);
|
|
|
|
|
for(int k = 0; k < total; k++)
|
|
|
|
|
rows[k] = all[k];
|
|
|
|
|
return total;
|
|
|
|
|
}
|
|
|
|
|
//--- UNIFORM stride, not the newest `cap` rows: taking a contiguous block would hand the models a
|
|
|
|
|
//--- narrower stretch of market than the net trained on, and any difference in score would then be
|
|
|
|
|
//--- partly a difference in regime.
|
|
|
|
|
double stride = (double)total / cap;
|
|
|
|
|
ArrayResize(rows, cap);
|
|
|
|
|
for(int k = 0; k < cap; k++)
|
|
|
|
|
rows[k] = all[(int)MathMin(total - 1, (int)(k * stride))];
|
|
|
|
|
return cap;
|
|
|
|
|
}
|
|
|
|
|
|
fix(baselines): the diagnostic held the EA thread long enough for MetaTrader to kill the process
Run_Alglib_Baselines was described as bounded because it 'cannot outrun the bar it runs on'. That
bound is four hours on H4, it was never enforced, and it is the wrong target: the EA is single
threaded, so while this pass runs there is no training, no panel, no tick handling, and a removal
request only queues.
Measured today on SP500 H4, 800 inputs x 4000 rows: forest 5 s, MLP 52 s, then the 3-fold CV -
three more full retrains. At 15:18:16, 83 s into it, MetaTrader force-terminated the EA. OnDeinit
never ran, so every arrow, panel object and label was orphaned on the chart. The init purge cleans
that up on re-attach; nothing in the pass writes to a cache, saves a model or feeds a decision, so
the damage was the freeze and the dirty chart, not corrupted state.
Three changes:
- BASELINE_MAX_CELLS. Every fit costs O(rows x width) and the row caps carried no width term.
Rows now shrink as the window widens - 1000 instead of 4000 at 800 columns - and the log says
when the cap bit.
- BASELINE_BUDGET_MS with BaselineBudgetSpent(), replacing the bare ShutdownRequested() calls at
each phase boundary. A stop and a spent budget need the same answer and only one was asked.
- The cross-validation is now declined BEFORE it starts, from its predicted cost - the measured MLP
time times the fold count. A boundary check cannot help once a phase is in flight, which is
exactly the phase that was in flight when the process died.
Also: the MLP now reports its weight count against its row count. At 800 inputs it carries ~6,400
weights, and today's run duly printed a training class error of 0.000. That is knowable from the
shape before fitting, so it is stated rather than left to a cross-validation that may not run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:22:42 -04:00
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Stop requested, or the pass has spent its wall clock. Replaces |
|
|
|
|
|
//| the bare ShutdownRequested() checks at every phase boundary: a |
|
|
|
|
|
//| stop and a blown budget need the same answer, and only one of |
|
|
|
|
|
//| them used to be asked. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
bool CBaselineComparator::BaselineBudgetSpent(const uint startTick, const string nextPhase)
|
fix(baselines): the diagnostic held the EA thread long enough for MetaTrader to kill the process
Run_Alglib_Baselines was described as bounded because it 'cannot outrun the bar it runs on'. That
bound is four hours on H4, it was never enforced, and it is the wrong target: the EA is single
threaded, so while this pass runs there is no training, no panel, no tick handling, and a removal
request only queues.
Measured today on SP500 H4, 800 inputs x 4000 rows: forest 5 s, MLP 52 s, then the 3-fold CV -
three more full retrains. At 15:18:16, 83 s into it, MetaTrader force-terminated the EA. OnDeinit
never ran, so every arrow, panel object and label was orphaned on the chart. The init purge cleans
that up on re-attach; nothing in the pass writes to a cache, saves a model or feeds a decision, so
the damage was the freeze and the dirty chart, not corrupted state.
Three changes:
- BASELINE_MAX_CELLS. Every fit costs O(rows x width) and the row caps carried no width term.
Rows now shrink as the window widens - 1000 instead of 4000 at 800 columns - and the log says
when the cap bit.
- BASELINE_BUDGET_MS with BaselineBudgetSpent(), replacing the bare ShutdownRequested() calls at
each phase boundary. A stop and a spent budget need the same answer and only one was asked.
- The cross-validation is now declined BEFORE it starts, from its predicted cost - the measured MLP
time times the fold count. A boundary check cannot help once a phase is in flight, which is
exactly the phase that was in flight when the process died.
Also: the MLP now reports its weight count against its row count. At 800 inputs it carries ~6,400
weights, and today's run duly printed a training class error of 0.000. That is knowable from the
shape before fitting, so it is stated rather than left to a cross-validation that may not run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:22:42 -04:00
|
|
|
{
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
if(m_data.Stopping())
|
fix(baselines): the diagnostic held the EA thread long enough for MetaTrader to kill the process
Run_Alglib_Baselines was described as bounded because it 'cannot outrun the bar it runs on'. That
bound is four hours on H4, it was never enforced, and it is the wrong target: the EA is single
threaded, so while this pass runs there is no training, no panel, no tick handling, and a removal
request only queues.
Measured today on SP500 H4, 800 inputs x 4000 rows: forest 5 s, MLP 52 s, then the 3-fold CV -
three more full retrains. At 15:18:16, 83 s into it, MetaTrader force-terminated the EA. OnDeinit
never ran, so every arrow, panel object and label was orphaned on the chart. The init purge cleans
that up on re-attach; nothing in the pass writes to a cache, saves a model or feeds a decision, so
the damage was the freeze and the dirty chart, not corrupted state.
Three changes:
- BASELINE_MAX_CELLS. Every fit costs O(rows x width) and the row caps carried no width term.
Rows now shrink as the window widens - 1000 instead of 4000 at 800 columns - and the log says
when the cap bit.
- BASELINE_BUDGET_MS with BaselineBudgetSpent(), replacing the bare ShutdownRequested() calls at
each phase boundary. A stop and a spent budget need the same answer and only one was asked.
- The cross-validation is now declined BEFORE it starts, from its predicted cost - the measured MLP
time times the fold count. A boundary check cannot help once a phase is in flight, which is
exactly the phase that was in flight when the process died.
Also: the MLP now reports its weight count against its row count. At 800 inputs it carries ~6,400
weights, and today's run duly printed a training class error of 0.000. That is knowable from the
shape before fitting, so it is stated rather than left to a cross-validation that may not run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:22:42 -04:00
|
|
|
return true;
|
|
|
|
|
uint spent = GetTickCount() - startTick;
|
|
|
|
|
if(spent < BASELINE_BUDGET_MS)
|
|
|
|
|
return false;
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + StringFormat(": Alglib baselines stopping before %s - %.1f s spent of a %.0f s budget."
|
fix(baselines): the diagnostic held the EA thread long enough for MetaTrader to kill the process
Run_Alglib_Baselines was described as bounded because it 'cannot outrun the bar it runs on'. That
bound is four hours on H4, it was never enforced, and it is the wrong target: the EA is single
threaded, so while this pass runs there is no training, no panel, no tick handling, and a removal
request only queues.
Measured today on SP500 H4, 800 inputs x 4000 rows: forest 5 s, MLP 52 s, then the 3-fold CV -
three more full retrains. At 15:18:16, 83 s into it, MetaTrader force-terminated the EA. OnDeinit
never ran, so every arrow, panel object and label was orphaned on the chart. The init purge cleans
that up on re-attach; nothing in the pass writes to a cache, saves a model or feeds a decision, so
the damage was the freeze and the dirty chart, not corrupted state.
Three changes:
- BASELINE_MAX_CELLS. Every fit costs O(rows x width) and the row caps carried no width term.
Rows now shrink as the window widens - 1000 instead of 4000 at 800 columns - and the log says
when the cap bit.
- BASELINE_BUDGET_MS with BaselineBudgetSpent(), replacing the bare ShutdownRequested() calls at
each phase boundary. A stop and a spent budget need the same answer and only one was asked.
- The cross-validation is now declined BEFORE it starts, from its predicted cost - the measured MLP
time times the fold count. A boundary check cannot help once a phase is in flight, which is
exactly the phase that was in flight when the process died.
Also: the MLP now reports its weight count against its row count. At 800 inputs it carries ~6,400
weights, and today's run duly printed a training class error of 0.000. That is knowable from the
shape before fitting, so it is stated rather than left to a cross-validation that may not run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:22:42 -04:00
|
|
|
" Everything printed above is complete; %s and the phases after it did not"
|
|
|
|
|
" run. The EA is single threaded, so this pass freezes training, the panel"
|
|
|
|
|
" and tick handling for exactly as long as it takes.",
|
|
|
|
|
nextPhase, spent / 1000.0, BASELINE_BUDGET_MS / 1000.0, nextPhase));
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
//+------------------------------------------------------------------+
|
2026-08-22 00:30:14 -04:00
|
|
|
//| TRAIN AND SCORE THE TWO NON-NN BASELINES. Once per run, at a |
|
|
|
|
|
//| pass-3 completion, and only when the trader asked for it. |
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
//+------------------------------------------------------------------+
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
void CBaselineComparator::RunBaselineComparison(const int bars, const int totalIter,
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
const int oosCutoff)
|
|
|
|
|
{
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
//--- THE pointer test for this whole module. Every private helper below is reached only from
|
|
|
|
|
//--- here, so guarding the one public entry guards all of them - and an unbound comparator is a
|
|
|
|
|
//--- wiring bug that must be loud, not a pass that quietly reports nothing.
|
|
|
|
|
if(CheckPointer(m_data) == POINTER_INVALID)
|
|
|
|
|
{
|
|
|
|
|
Print(__FUNCTION__ + ": ERROR - baseline comparator has no data view bound. No baselines this"
|
|
|
|
|
" run; the models are unaffected but the MATRIX-vs-IMPLEMENTATION question goes"
|
|
|
|
|
" unanswered.");
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
return;
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
}
|
|
|
|
|
if(!Run_Alglib_Baselines || m_done)
|
|
|
|
|
return;
|
|
|
|
|
m_done = true; // set FIRST: a run that bails below must not retry every era
|
|
|
|
|
if(m_data.IsEnsembleMember() && g_ensembleChartBaselinesDone)
|
2026-08-20 14:04:06 -04:00
|
|
|
{
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + ": Alglib baselines already measured by another ensemble member on this chart - same"
|
2026-08-20 14:04:06 -04:00
|
|
|
" windows, same labels, same three fits, same answer. Skipped; the first member's report"
|
|
|
|
|
" above is this model's too.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
g_ensembleChartBaselinesDone = true;
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
int width = m_data.HistoryBars() * m_data.FeaturesPerBar();
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
if(width <= 0 || oosCutoff <= 0 || totalIter <= oosCutoff)
|
|
|
|
|
{
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + ": Alglib baselines skipped - no usable era geometry yet.");
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
return;
|
|
|
|
|
}
|
fix(baselines): the diagnostic held the EA thread long enough for MetaTrader to kill the process
Run_Alglib_Baselines was described as bounded because it 'cannot outrun the bar it runs on'. That
bound is four hours on H4, it was never enforced, and it is the wrong target: the EA is single
threaded, so while this pass runs there is no training, no panel, no tick handling, and a removal
request only queues.
Measured today on SP500 H4, 800 inputs x 4000 rows: forest 5 s, MLP 52 s, then the 3-fold CV -
three more full retrains. At 15:18:16, 83 s into it, MetaTrader force-terminated the EA. OnDeinit
never ran, so every arrow, panel object and label was orphaned on the chart. The init purge cleans
that up on re-attach; nothing in the pass writes to a cache, saves a model or feeds a decision, so
the damage was the freeze and the dirty chart, not corrupted state.
Three changes:
- BASELINE_MAX_CELLS. Every fit costs O(rows x width) and the row caps carried no width term.
Rows now shrink as the window widens - 1000 instead of 4000 at 800 columns - and the log says
when the cap bit.
- BASELINE_BUDGET_MS with BaselineBudgetSpent(), replacing the bare ShutdownRequested() calls at
each phase boundary. A stop and a spent budget need the same answer and only one was asked.
- The cross-validation is now declined BEFORE it starts, from its predicted cost - the measured MLP
time times the fold count. A boundary check cannot help once a phase is in flight, which is
exactly the phase that was in flight when the process died.
Also: the MLP now reports its weight count against its row count. At 800 inputs it carries ~6,400
weights, and today's run duly printed a training class error of 0.000. That is knowable from the
shape before fitting, so it is stated rather than left to a cross-validation that may not run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:22:42 -04:00
|
|
|
uint t0 = GetTickCount();
|
|
|
|
|
if(BaselineBudgetSpent(t0, "the matrix build"))
|
2026-08-20 00:34:55 -04:00
|
|
|
return;
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
//--- SAME SPLIT AS THE NET, read off the same helpers rather than recomputed here. IS starts one
|
|
|
|
|
//--- purge past the calibration band's far edge, exactly where the backprop queue starts.
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
int isLo = m_data.CalibrationHiIndex(totalIter, oosCutoff) + m_data.PurgeBars();
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
int trainBars[], scoreBars[];
|
fix(baselines): the diagnostic held the EA thread long enough for MetaTrader to kill the process
Run_Alglib_Baselines was described as bounded because it 'cannot outrun the bar it runs on'. That
bound is four hours on H4, it was never enforced, and it is the wrong target: the EA is single
threaded, so while this pass runs there is no training, no panel, no tick handling, and a removal
request only queues.
Measured today on SP500 H4, 800 inputs x 4000 rows: forest 5 s, MLP 52 s, then the 3-fold CV -
three more full retrains. At 15:18:16, 83 s into it, MetaTrader force-terminated the EA. OnDeinit
never ran, so every arrow, panel object and label was orphaned on the chart. The init purge cleans
that up on re-attach; nothing in the pass writes to a cache, saves a model or feeds a decision, so
the damage was the freeze and the dirty chart, not corrupted state.
Three changes:
- BASELINE_MAX_CELLS. Every fit costs O(rows x width) and the row caps carried no width term.
Rows now shrink as the window widens - 1000 instead of 4000 at 800 columns - and the log says
when the cap bit.
- BASELINE_BUDGET_MS with BaselineBudgetSpent(), replacing the bare ShutdownRequested() calls at
each phase boundary. A stop and a spent budget need the same answer and only one was asked.
- The cross-validation is now declined BEFORE it starts, from its predicted cost - the measured MLP
time times the fold count. A boundary check cannot help once a phase is in flight, which is
exactly the phase that was in flight when the process died.
Also: the MLP now reports its weight count against its row count. At 800 inputs it carries ~6,400
weights, and today's run duly printed a training class error of 0.000. That is knowable from the
shape before fitting, so it is stated rather than left to a cross-validation that may not run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:22:42 -04:00
|
|
|
//--- Rows shrink as the window widens - see BASELINE_MAX_CELLS. Floored at BASELINE_MIN_ROWS so a
|
|
|
|
|
//--- very wide window skips below on "too few rows to say anything" rather than silently fitting one.
|
|
|
|
|
int rowCap = (int)MathMax(BASELINE_MAX_CELLS / width, BASELINE_MIN_ROWS);
|
|
|
|
|
int trainCap = MathMin(BASELINE_MAX_TRAIN_ROWS, rowCap);
|
|
|
|
|
int scoreCap = MathMin(BASELINE_MAX_SCORE_ROWS, rowCap);
|
|
|
|
|
if(trainCap < BASELINE_MAX_TRAIN_ROWS)
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + StringFormat(": Alglib baselines - %d-column window caps this run at %d rows (was %d)."
|
fix(baselines): the diagnostic held the EA thread long enough for MetaTrader to kill the process
Run_Alglib_Baselines was described as bounded because it 'cannot outrun the bar it runs on'. That
bound is four hours on H4, it was never enforced, and it is the wrong target: the EA is single
threaded, so while this pass runs there is no training, no panel, no tick handling, and a removal
request only queues.
Measured today on SP500 H4, 800 inputs x 4000 rows: forest 5 s, MLP 52 s, then the 3-fold CV -
three more full retrains. At 15:18:16, 83 s into it, MetaTrader force-terminated the EA. OnDeinit
never ran, so every arrow, panel object and label was orphaned on the chart. The init purge cleans
that up on re-attach; nothing in the pass writes to a cache, saves a model or feeds a decision, so
the damage was the freeze and the dirty chart, not corrupted state.
Three changes:
- BASELINE_MAX_CELLS. Every fit costs O(rows x width) and the row caps carried no width term.
Rows now shrink as the window widens - 1000 instead of 4000 at 800 columns - and the log says
when the cap bit.
- BASELINE_BUDGET_MS with BaselineBudgetSpent(), replacing the bare ShutdownRequested() calls at
each phase boundary. A stop and a spent budget need the same answer and only one was asked.
- The cross-validation is now declined BEFORE it starts, from its predicted cost - the measured MLP
time times the fold count. A boundary check cannot help once a phase is in flight, which is
exactly the phase that was in flight when the process died.
Also: the MLP now reports its weight count against its row count. At 800 inputs it carries ~6,400
weights, and today's run duly printed a training class error of 0.000. That is knowable from the
shape before fitting, so it is stated rather than left to a cross-validation that may not run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:22:42 -04:00
|
|
|
" Every fit costs rows x columns, and the un-capped design is what got the"
|
|
|
|
|
" EA force-terminated on 2026-08-21.",
|
|
|
|
|
width, trainCap, BASELINE_MAX_TRAIN_ROWS));
|
|
|
|
|
int nTrainWanted = BaselineCandidateBars(isLo, totalIter, bars, trainCap, trainBars);
|
|
|
|
|
int nScoreWanted = BaselineCandidateBars(0, oosCutoff, bars, scoreCap, scoreBars);
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
if(nTrainWanted < BASELINE_MIN_ROWS || nScoreWanted < BASELINE_MIN_ROWS)
|
|
|
|
|
{
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + StringFormat(": Alglib baselines skipped - %d train / %d score bars carry a label,"
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
" below the %d needed to say anything.",
|
|
|
|
|
nTrainWanted, nScoreWanted, BASELINE_MIN_ROWS));
|
|
|
|
|
return;
|
|
|
|
|
}
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + StringFormat(": Alglib baselines starting - %d inputs (%d bars x %d features),"
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
" %d train rows, %d OOS rows. Same windows, labels and split as the net.",
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
width, m_data.HistoryBars(), m_data.FeaturesPerBar(), nTrainWanted, nScoreWanted));
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
//--- BUILD THE TRAIN MATRIX. Last column is the 3-class target the forest wants (0=Sell, 1=Neutral,
|
|
|
|
|
//--- 2=Buy); the linear fit reuses the same allocation with that column shifted to -1/0/+1 below,
|
|
|
|
|
//--- so the two models cannot end up looking at different rows.
|
|
|
|
|
CMatrixDouble xy(nTrainWanted, width + 1);
|
2026-08-22 00:25:52 -04:00
|
|
|
//--- ...and, at no extra feature cost, the ANCHOR BAR'S OWN ROW alone. BuildFeatureWindow
|
|
|
|
|
//--- appends deepest-lookback first and lands on the anchor last, so those are the final
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
//--- FeaturesPerBar() values of every window.
|
|
|
|
|
CMatrixDouble anchors(nTrainWanted, m_data.FeaturesPerBar());
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
double x[];
|
|
|
|
|
ArrayResize(x, width);
|
|
|
|
|
int nTrain = 0;
|
|
|
|
|
for(int k = 0; k < nTrainWanted; k++)
|
|
|
|
|
{
|
|
|
|
|
int r = trainBars[k];
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
if(!m_data.RowFeatures(r, width, x))
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
continue;
|
|
|
|
|
for(int f = 0; f < width; f++)
|
|
|
|
|
xy.Set(nTrain, f, x[f]);
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
for(int f = 0; f < m_data.FeaturesPerBar(); f++)
|
|
|
|
|
anchors.Set(nTrain, f, x[width - m_data.FeaturesPerBar() + f]);
|
|
|
|
|
double cls = m_data.IsBuyLabel(r) ? 2.0 : (m_data.IsSellLabel(r) ? 0.0 : 1.0);
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
xy.Set(nTrain, width, cls);
|
|
|
|
|
nTrain++;
|
|
|
|
|
}
|
fix(baselines): the diagnostic held the EA thread long enough for MetaTrader to kill the process
Run_Alglib_Baselines was described as bounded because it 'cannot outrun the bar it runs on'. That
bound is four hours on H4, it was never enforced, and it is the wrong target: the EA is single
threaded, so while this pass runs there is no training, no panel, no tick handling, and a removal
request only queues.
Measured today on SP500 H4, 800 inputs x 4000 rows: forest 5 s, MLP 52 s, then the 3-fold CV -
three more full retrains. At 15:18:16, 83 s into it, MetaTrader force-terminated the EA. OnDeinit
never ran, so every arrow, panel object and label was orphaned on the chart. The init purge cleans
that up on re-attach; nothing in the pass writes to a cache, saves a model or feeds a decision, so
the damage was the freeze and the dirty chart, not corrupted state.
Three changes:
- BASELINE_MAX_CELLS. Every fit costs O(rows x width) and the row caps carried no width term.
Rows now shrink as the window widens - 1000 instead of 4000 at 800 columns - and the log says
when the cap bit.
- BASELINE_BUDGET_MS with BaselineBudgetSpent(), replacing the bare ShutdownRequested() calls at
each phase boundary. A stop and a spent budget need the same answer and only one was asked.
- The cross-validation is now declined BEFORE it starts, from its predicted cost - the measured MLP
time times the fold count. A boundary check cannot help once a phase is in flight, which is
exactly the phase that was in flight when the process died.
Also: the MLP now reports its weight count against its row count. At 800 inputs it carries ~6,400
weights, and today's run duly printed a training class error of 0.000. That is knowable from the
shape before fitting, so it is stated rather than left to a cross-validation that may not run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:22:42 -04:00
|
|
|
if(nTrain < BASELINE_MIN_ROWS || BaselineBudgetSpent(t0, "the forest"))
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
{
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + StringFormat(": Alglib baselines abandoned - only %d of %d train windows built.",
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
nTrain, nTrainWanted));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
//--- FOREST. Builder object, not the deprecated DFBuildRandomDecisionForest one-shot. Variables per
|
|
|
|
|
//--- split are left on the library's own auto rule: with a window this wide any hand-picked number
|
|
|
|
|
//--- would be a tuned knob, and a baseline that needed tuning to lose would prove nothing.
|
|
|
|
|
CDecisionForestBuilder builder;
|
|
|
|
|
CDecisionForest forest;
|
|
|
|
|
CDFReport frep;
|
|
|
|
|
CDForest::DFBuilderCreate(builder);
|
|
|
|
|
CDForest::DFBuilderSetDataset(builder, xy, nTrain, width, 3);
|
|
|
|
|
CDForest::DFBuilderSetSubsampleRatio(builder, BASELINE_SUBSAMPLE_RATIO);
|
|
|
|
|
CDForest::DFBuilderSetRndVarsAuto(builder);
|
|
|
|
|
CDForest::DFBuilderSetSeed(builder, 1);
|
|
|
|
|
CDForest::DFBuilderSetImportanceNone(builder);
|
|
|
|
|
CDForest::DFBuilderBuildRandomForest(builder, BASELINE_TREES, forest, frep);
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + StringFormat(": forest built - %d trees, out-of-bag class error %.3f, OOB avg CE %.4f.",
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
BASELINE_TREES, frep.m_oobrelclserror, frep.m_oobavgce));
|
fix(baselines): the diagnostic held the EA thread long enough for MetaTrader to kill the process
Run_Alglib_Baselines was described as bounded because it 'cannot outrun the bar it runs on'. That
bound is four hours on H4, it was never enforced, and it is the wrong target: the EA is single
threaded, so while this pass runs there is no training, no panel, no tick handling, and a removal
request only queues.
Measured today on SP500 H4, 800 inputs x 4000 rows: forest 5 s, MLP 52 s, then the 3-fold CV -
three more full retrains. At 15:18:16, 83 s into it, MetaTrader force-terminated the EA. OnDeinit
never ran, so every arrow, panel object and label was orphaned on the chart. The init purge cleans
that up on re-attach; nothing in the pass writes to a cache, saves a model or feeds a decision, so
the damage was the freeze and the dirty chart, not corrupted state.
Three changes:
- BASELINE_MAX_CELLS. Every fit costs O(rows x width) and the row caps carried no width term.
Rows now shrink as the window widens - 1000 instead of 4000 at 800 columns - and the log says
when the cap bit.
- BASELINE_BUDGET_MS with BaselineBudgetSpent(), replacing the bare ShutdownRequested() calls at
each phase boundary. A stop and a spent budget need the same answer and only one was asked.
- The cross-validation is now declined BEFORE it starts, from its predicted cost - the measured MLP
time times the fold count. A boundary check cannot help once a phase is in flight, which is
exactly the phase that was in flight when the process died.
Also: the MLP now reports its weight count against its row count. At 800 inputs it carries ~6,400
weights, and today's run duly printed a training class error of 0.000. That is knowable from the
shape before fitting, so it is stated rather than left to a cross-validation that may not run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:22:42 -04:00
|
|
|
if(BaselineBudgetSpent(t0, "the MLP"))
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
return;
|
2026-08-22 00:25:52 -04:00
|
|
|
//--- MLP. ALGLIB's own multilayer perceptron on the identical matrix - the control that
|
|
|
|
|
//--- separates "our architecture is wrong" from "our IMPLEMENTATION is wrong".
|
feat(baselines): ALGLIB's MLP as a third baseline, not a replacement
Asked whether ALGLIB's perceptron should replace ours "only if it is
better". This measures that instead of assuming it, and the measurement
is worth more than the swap would have been.
The forest and the linear fit test whether the MATRIX carries direction.
Neither tests whether OUR CODE is what fails. ALGLIB's MLP does: it is a
long-standing independent implementation of roughly what our own dense
stack computes, trained on the identical rows through the identical
gate. That distinction is not hypothetical here - two silent
implementation faults have already invalidated every model-based
negative behind them (a transposed dense hidden gradient, so backprop
was never backprop; and Adam storing sqrt(v) and feeding it back as
variance, so every "Adam" run was SGD). If alglib-mlp clears a bar our
own net cannot on the same rows, the fault is in our code.
Trained BEFORE the target column is rewritten for the linear fit:
MLPCreateC1 builds a softmax classifier whose trainer wants the class
index, which is what the matrix still holds at that point. Scored by
argmax with Neutral abstaining, exactly as the forest is. Sidak family
is now 3, not 2.
Deliberately NOT a replacement, and the gap is not close: ALGLIB's MLP
is feed-forward only (no conv, no recurrence - so it could not stand in
for three of the four members at all), and it has no batch norm, no
logit-adjusted loss for the class imbalance, no era resumption, no
OpenCL or CPU-DLL backend and no .nnw. Adopting it would trade a
maintained architecture for a control.
Kept small on purpose - 8 hidden units, 1 restart, 100 iterations, decay
at the 0.001 the ALGLIB docs recommend when you have no reason to pick
another. A wide net would spend the six-core box's hours answering a
question about capacity when the question is about implementation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:19:53 -04:00
|
|
|
CMultilayerPerceptron mlp;
|
|
|
|
|
CMLPReport mrep;
|
|
|
|
|
int mlpInfo = 0;
|
|
|
|
|
CMLPBase::MLPCreateC1(width, BASELINE_MLP_HIDDEN, 3, mlp);
|
fix(baselines): the diagnostic held the EA thread long enough for MetaTrader to kill the process
Run_Alglib_Baselines was described as bounded because it 'cannot outrun the bar it runs on'. That
bound is four hours on H4, it was never enforced, and it is the wrong target: the EA is single
threaded, so while this pass runs there is no training, no panel, no tick handling, and a removal
request only queues.
Measured today on SP500 H4, 800 inputs x 4000 rows: forest 5 s, MLP 52 s, then the 3-fold CV -
three more full retrains. At 15:18:16, 83 s into it, MetaTrader force-terminated the EA. OnDeinit
never ran, so every arrow, panel object and label was orphaned on the chart. The init purge cleans
that up on re-attach; nothing in the pass writes to a cache, saves a model or feeds a decision, so
the damage was the freeze and the dirty chart, not corrupted state.
Three changes:
- BASELINE_MAX_CELLS. Every fit costs O(rows x width) and the row caps carried no width term.
Rows now shrink as the window widens - 1000 instead of 4000 at 800 columns - and the log says
when the cap bit.
- BASELINE_BUDGET_MS with BaselineBudgetSpent(), replacing the bare ShutdownRequested() calls at
each phase boundary. A stop and a spent budget need the same answer and only one was asked.
- The cross-validation is now declined BEFORE it starts, from its predicted cost - the measured MLP
time times the fold count. A boundary check cannot help once a phase is in flight, which is
exactly the phase that was in flight when the process died.
Also: the MLP now reports its weight count against its row count. At 800 inputs it carries ~6,400
weights, and today's run duly printed a training class error of 0.000. That is knowable from the
shape before fitting, so it is stated rather than left to a cross-validation that may not run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:22:42 -04:00
|
|
|
uint mlpStart = GetTickCount();
|
feat(baselines): ALGLIB's MLP as a third baseline, not a replacement
Asked whether ALGLIB's perceptron should replace ours "only if it is
better". This measures that instead of assuming it, and the measurement
is worth more than the swap would have been.
The forest and the linear fit test whether the MATRIX carries direction.
Neither tests whether OUR CODE is what fails. ALGLIB's MLP does: it is a
long-standing independent implementation of roughly what our own dense
stack computes, trained on the identical rows through the identical
gate. That distinction is not hypothetical here - two silent
implementation faults have already invalidated every model-based
negative behind them (a transposed dense hidden gradient, so backprop
was never backprop; and Adam storing sqrt(v) and feeding it back as
variance, so every "Adam" run was SGD). If alglib-mlp clears a bar our
own net cannot on the same rows, the fault is in our code.
Trained BEFORE the target column is rewritten for the linear fit:
MLPCreateC1 builds a softmax classifier whose trainer wants the class
index, which is what the matrix still holds at that point. Scored by
argmax with Neutral abstaining, exactly as the forest is. Sidak family
is now 3, not 2.
Deliberately NOT a replacement, and the gap is not close: ALGLIB's MLP
is feed-forward only (no conv, no recurrence - so it could not stand in
for three of the four members at all), and it has no batch norm, no
logit-adjusted loss for the class imbalance, no era resumption, no
OpenCL or CPU-DLL backend and no .nnw. Adopting it would trade a
maintained architecture for a control.
Kept small on purpose - 8 hidden units, 1 restart, 100 iterations, decay
at the 0.001 the ALGLIB docs recommend when you have no reason to pick
another. A wide net would spend the six-core box's hours answering a
question about capacity when the question is about implementation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:19:53 -04:00
|
|
|
CMLPTrain::MLPTrainLBFGS(mlp, xy, nTrain, BASELINE_MLP_DECAY, BASELINE_MLP_RESTARTS,
|
|
|
|
|
BASELINE_MLP_WSTEP, BASELINE_MLP_MAXITS, mlpInfo, mrep);
|
fix(baselines): the diagnostic held the EA thread long enough for MetaTrader to kill the process
Run_Alglib_Baselines was described as bounded because it 'cannot outrun the bar it runs on'. That
bound is four hours on H4, it was never enforced, and it is the wrong target: the EA is single
threaded, so while this pass runs there is no training, no panel, no tick handling, and a removal
request only queues.
Measured today on SP500 H4, 800 inputs x 4000 rows: forest 5 s, MLP 52 s, then the 3-fold CV -
three more full retrains. At 15:18:16, 83 s into it, MetaTrader force-terminated the EA. OnDeinit
never ran, so every arrow, panel object and label was orphaned on the chart. The init purge cleans
that up on re-attach; nothing in the pass writes to a cache, saves a model or feeds a decision, so
the damage was the freeze and the dirty chart, not corrupted state.
Three changes:
- BASELINE_MAX_CELLS. Every fit costs O(rows x width) and the row caps carried no width term.
Rows now shrink as the window widens - 1000 instead of 4000 at 800 columns - and the log says
when the cap bit.
- BASELINE_BUDGET_MS with BaselineBudgetSpent(), replacing the bare ShutdownRequested() calls at
each phase boundary. A stop and a spent budget need the same answer and only one was asked.
- The cross-validation is now declined BEFORE it starts, from its predicted cost - the measured MLP
time times the fold count. A boundary check cannot help once a phase is in flight, which is
exactly the phase that was in flight when the process died.
Also: the MLP now reports its weight count against its row count. At 800 inputs it carries ~6,400
weights, and today's run duly printed a training class error of 0.000. That is knowable from the
shape before fitting, so it is stated rather than left to a cross-validation that may not run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:22:42 -04:00
|
|
|
uint mlpMs = GetTickCount() - mlpStart;
|
feat(baselines): ALGLIB's MLP as a third baseline, not a replacement
Asked whether ALGLIB's perceptron should replace ours "only if it is
better". This measures that instead of assuming it, and the measurement
is worth more than the swap would have been.
The forest and the linear fit test whether the MATRIX carries direction.
Neither tests whether OUR CODE is what fails. ALGLIB's MLP does: it is a
long-standing independent implementation of roughly what our own dense
stack computes, trained on the identical rows through the identical
gate. That distinction is not hypothetical here - two silent
implementation faults have already invalidated every model-based
negative behind them (a transposed dense hidden gradient, so backprop
was never backprop; and Adam storing sqrt(v) and feeding it back as
variance, so every "Adam" run was SGD). If alglib-mlp clears a bar our
own net cannot on the same rows, the fault is in our code.
Trained BEFORE the target column is rewritten for the linear fit:
MLPCreateC1 builds a softmax classifier whose trainer wants the class
index, which is what the matrix still holds at that point. Scored by
argmax with Neutral abstaining, exactly as the forest is. Sidak family
is now 3, not 2.
Deliberately NOT a replacement, and the gap is not close: ALGLIB's MLP
is feed-forward only (no conv, no recurrence - so it could not stand in
for three of the four members at all), and it has no batch norm, no
logit-adjusted loss for the class imbalance, no era resumption, no
OpenCL or CPU-DLL backend and no .nnw. Adopting it would trade a
maintained architecture for a control.
Kept small on purpose - 8 hidden units, 1 restart, 100 iterations, decay
at the 0.001 the ALGLIB docs recommend when you have no reason to pick
another. A wide net would spend the six-core box's hours answering a
question about capacity when the question is about implementation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:19:53 -04:00
|
|
|
bool mlpOK = (mlpInfo > 0);
|
2026-08-22 00:25:52 -04:00
|
|
|
//--- CAPACITY FIRST, because it decides what the training error below is allowed to mean.
|
fix(baselines): the diagnostic held the EA thread long enough for MetaTrader to kill the process
Run_Alglib_Baselines was described as bounded because it 'cannot outrun the bar it runs on'. That
bound is four hours on H4, it was never enforced, and it is the wrong target: the EA is single
threaded, so while this pass runs there is no training, no panel, no tick handling, and a removal
request only queues.
Measured today on SP500 H4, 800 inputs x 4000 rows: forest 5 s, MLP 52 s, then the 3-fold CV -
three more full retrains. At 15:18:16, 83 s into it, MetaTrader force-terminated the EA. OnDeinit
never ran, so every arrow, panel object and label was orphaned on the chart. The init purge cleans
that up on re-attach; nothing in the pass writes to a cache, saves a model or feeds a decision, so
the damage was the freeze and the dirty chart, not corrupted state.
Three changes:
- BASELINE_MAX_CELLS. Every fit costs O(rows x width) and the row caps carried no width term.
Rows now shrink as the window widens - 1000 instead of 4000 at 800 columns - and the log says
when the cap bit.
- BASELINE_BUDGET_MS with BaselineBudgetSpent(), replacing the bare ShutdownRequested() calls at
each phase boundary. A stop and a spent budget need the same answer and only one was asked.
- The cross-validation is now declined BEFORE it starts, from its predicted cost - the measured MLP
time times the fold count. A boundary check cannot help once a phase is in flight, which is
exactly the phase that was in flight when the process died.
Also: the MLP now reports its weight count against its row count. At 800 inputs it carries ~6,400
weights, and today's run duly printed a training class error of 0.000. That is knowable from the
shape before fitting, so it is stated rather than left to a cross-validation that may not run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:22:42 -04:00
|
|
|
int mlpWeights = (width + 1) * BASELINE_MLP_HIDDEN + (BASELINE_MLP_HIDDEN + 1) * 3;
|
feat(baselines): ALGLIB's MLP as a third baseline, not a replacement
Asked whether ALGLIB's perceptron should replace ours "only if it is
better". This measures that instead of assuming it, and the measurement
is worth more than the swap would have been.
The forest and the linear fit test whether the MATRIX carries direction.
Neither tests whether OUR CODE is what fails. ALGLIB's MLP does: it is a
long-standing independent implementation of roughly what our own dense
stack computes, trained on the identical rows through the identical
gate. That distinction is not hypothetical here - two silent
implementation faults have already invalidated every model-based
negative behind them (a transposed dense hidden gradient, so backprop
was never backprop; and Adam storing sqrt(v) and feeding it back as
variance, so every "Adam" run was SGD). If alglib-mlp clears a bar our
own net cannot on the same rows, the fault is in our code.
Trained BEFORE the target column is rewritten for the linear fit:
MLPCreateC1 builds a softmax classifier whose trainer wants the class
index, which is what the matrix still holds at that point. Scored by
argmax with Neutral abstaining, exactly as the forest is. Sidak family
is now 3, not 2.
Deliberately NOT a replacement, and the gap is not close: ALGLIB's MLP
is feed-forward only (no conv, no recurrence - so it could not stand in
for three of the four members at all), and it has no batch norm, no
logit-adjusted loss for the class imbalance, no era resumption, no
OpenCL or CPU-DLL backend and no .nnw. Adopting it would trade a
maintained architecture for a control.
Kept small on purpose - 8 hidden units, 1 restart, 100 iterations, decay
at the 0.001 the ALGLIB docs recommend when you have no reason to pick
another. A wide net would spend the six-core box's hours answering a
question about capacity when the question is about implementation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:19:53 -04:00
|
|
|
if(mlpOK)
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + StringFormat(": ALGLIB MLP trained in %.1f s - %d inputs -> %d hidden -> 3, %d gradient"
|
fix(baselines): the diagnostic held the EA thread long enough for MetaTrader to kill the process
Run_Alglib_Baselines was described as bounded because it 'cannot outrun the bar it runs on'. That
bound is four hours on H4, it was never enforced, and it is the wrong target: the EA is single
threaded, so while this pass runs there is no training, no panel, no tick handling, and a removal
request only queues.
Measured today on SP500 H4, 800 inputs x 4000 rows: forest 5 s, MLP 52 s, then the 3-fold CV -
three more full retrains. At 15:18:16, 83 s into it, MetaTrader force-terminated the EA. OnDeinit
never ran, so every arrow, panel object and label was orphaned on the chart. The init purge cleans
that up on re-attach; nothing in the pass writes to a cache, saves a model or feeds a decision, so
the damage was the freeze and the dirty chart, not corrupted state.
Three changes:
- BASELINE_MAX_CELLS. Every fit costs O(rows x width) and the row caps carried no width term.
Rows now shrink as the window widens - 1000 instead of 4000 at 800 columns - and the log says
when the cap bit.
- BASELINE_BUDGET_MS with BaselineBudgetSpent(), replacing the bare ShutdownRequested() calls at
each phase boundary. A stop and a spent budget need the same answer and only one was asked.
- The cross-validation is now declined BEFORE it starts, from its predicted cost - the measured MLP
time times the fold count. A boundary check cannot help once a phase is in flight, which is
exactly the phase that was in flight when the process died.
Also: the MLP now reports its weight count against its row count. At 800 inputs it carries ~6,400
weights, and today's run duly printed a training class error of 0.000. That is knowable from the
shape before fitting, so it is stated rather than left to a cross-validation that may not run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:22:42 -04:00
|
|
|
" evaluations, training class error %.3f, avg CE %.4f.%s",
|
|
|
|
|
mlpMs / 1000.0, width, BASELINE_MLP_HIDDEN, mrep.m_ngrad,
|
|
|
|
|
mrep.m_RelCLSError, mrep.m_AvgCE,
|
|
|
|
|
(mlpWeights >= nTrain)
|
|
|
|
|
? StringFormat(" NOTE: %d weights against %d rows - this fit can memorise"
|
|
|
|
|
" its own rows outright, so read the OOS score below and"
|
|
|
|
|
" not the training error.", mlpWeights, nTrain)
|
|
|
|
|
: ""));
|
feat(baselines): ALGLIB's MLP as a third baseline, not a replacement
Asked whether ALGLIB's perceptron should replace ours "only if it is
better". This measures that instead of assuming it, and the measurement
is worth more than the swap would have been.
The forest and the linear fit test whether the MATRIX carries direction.
Neither tests whether OUR CODE is what fails. ALGLIB's MLP does: it is a
long-standing independent implementation of roughly what our own dense
stack computes, trained on the identical rows through the identical
gate. That distinction is not hypothetical here - two silent
implementation faults have already invalidated every model-based
negative behind them (a transposed dense hidden gradient, so backprop
was never backprop; and Adam storing sqrt(v) and feeding it back as
variance, so every "Adam" run was SGD). If alglib-mlp clears a bar our
own net cannot on the same rows, the fault is in our code.
Trained BEFORE the target column is rewritten for the linear fit:
MLPCreateC1 builds a softmax classifier whose trainer wants the class
index, which is what the matrix still holds at that point. Scored by
argmax with Neutral abstaining, exactly as the forest is. Sidak family
is now 3, not 2.
Deliberately NOT a replacement, and the gap is not close: ALGLIB's MLP
is feed-forward only (no conv, no recurrence - so it could not stand in
for three of the four members at all), and it has no batch norm, no
logit-adjusted loss for the class imbalance, no era resumption, no
OpenCL or CPU-DLL backend and no .nnw. Adopting it would trade a
maintained architecture for a control.
Kept small on purpose - 8 hidden units, 1 restart, 100 iterations, decay
at the 0.001 the ALGLIB docs recommend when you have no reason to pick
another. A wide net would spend the six-core box's hours answering a
question about capacity when the question is about implementation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:19:53 -04:00
|
|
|
else
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + StringFormat(": ALGLIB MLP did not train (info %d) - skipped below.", mlpInfo));
|
fix(baselines): the diagnostic held the EA thread long enough for MetaTrader to kill the process
Run_Alglib_Baselines was described as bounded because it 'cannot outrun the bar it runs on'. That
bound is four hours on H4, it was never enforced, and it is the wrong target: the EA is single
threaded, so while this pass runs there is no training, no panel, no tick handling, and a removal
request only queues.
Measured today on SP500 H4, 800 inputs x 4000 rows: forest 5 s, MLP 52 s, then the 3-fold CV -
three more full retrains. At 15:18:16, 83 s into it, MetaTrader force-terminated the EA. OnDeinit
never ran, so every arrow, panel object and label was orphaned on the chart. The init purge cleans
that up on re-attach; nothing in the pass writes to a cache, saves a model or feeds a decision, so
the damage was the freeze and the dirty chart, not corrupted state.
Three changes:
- BASELINE_MAX_CELLS. Every fit costs O(rows x width) and the row caps carried no width term.
Rows now shrink as the window widens - 1000 instead of 4000 at 800 columns - and the log says
when the cap bit.
- BASELINE_BUDGET_MS with BaselineBudgetSpent(), replacing the bare ShutdownRequested() calls at
each phase boundary. A stop and a spent budget need the same answer and only one was asked.
- The cross-validation is now declined BEFORE it starts, from its predicted cost - the measured MLP
time times the fold count. A boundary check cannot help once a phase is in flight, which is
exactly the phase that was in flight when the process died.
Also: the MLP now reports its weight count against its row count. At 800 inputs it carries ~6,400
weights, and today's run duly printed a training class error of 0.000. That is knowable from the
shape before fitting, so it is stated rather than left to a cross-validation that may not run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:22:42 -04:00
|
|
|
if(BaselineBudgetSpent(t0, "the cross-validation"))
|
feat(baselines): ALGLIB's MLP as a third baseline, not a replacement
Asked whether ALGLIB's perceptron should replace ours "only if it is
better". This measures that instead of assuming it, and the measurement
is worth more than the swap would have been.
The forest and the linear fit test whether the MATRIX carries direction.
Neither tests whether OUR CODE is what fails. ALGLIB's MLP does: it is a
long-standing independent implementation of roughly what our own dense
stack computes, trained on the identical rows through the identical
gate. That distinction is not hypothetical here - two silent
implementation faults have already invalidated every model-based
negative behind them (a transposed dense hidden gradient, so backprop
was never backprop; and Adam storing sqrt(v) and feeding it back as
variance, so every "Adam" run was SGD). If alglib-mlp clears a bar our
own net cannot on the same rows, the fault is in our code.
Trained BEFORE the target column is rewritten for the linear fit:
MLPCreateC1 builds a softmax classifier whose trainer wants the class
index, which is what the matrix still holds at that point. Scored by
argmax with Neutral abstaining, exactly as the forest is. Sidak family
is now 3, not 2.
Deliberately NOT a replacement, and the gap is not close: ALGLIB's MLP
is feed-forward only (no conv, no recurrence - so it could not stand in
for three of the four members at all), and it has no batch norm, no
logit-adjusted loss for the class imbalance, no era resumption, no
OpenCL or CPU-DLL backend and no .nnw. Adopting it would trade a
maintained architecture for a control.
Kept small on purpose - 8 hidden units, 1 restart, 100 iterations, decay
at the 0.001 the ALGLIB docs recommend when you have no reason to pick
another. A wide net would spend the six-core box's hours answering a
question about capacity when the question is about implementation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:19:53 -04:00
|
|
|
return;
|
feat(baselines): combining-weight fit, MLP cross-validation, all-lags correlation
Three ALGLIB additions, all measurement-only and all under the existing
Run_Alglib_Baselines switch.
MinBLEIC COMBINING WEIGHTS. The live ensemble weights each member by its
own pooled holdout win rate - a defensible prior, but not a fit, and
nothing has ever asked what mixture minimises error on the bars the
members disagreed about. Two individually-mediocre members wrong in
different places can beat one individually better, and a per-member win
rate cannot express that because it never looks at them jointly.
Solved on the simplex (w >= 0, sum w = 1), which is exactly what
MinBLEIC is for. Non-negative because a negative weight asserts "trade
the opposite of this member", a claim ~60 effective observations cannot
support. Least squares on the signed outcome rather than precision:
precision is a STEP function of the threshold that no gradient method
can walk, and optimising a smooth proxy for a step decision is how
c3daded put every operating point 14pp underwater - so the result is
reported in BOTH currencies, the SSE it minimised and the directional
hit rate the mixture would actually have scored against the equal mix.
If the second does not improve, the first is noise.
This needed data that did not exist: g_ensVoteSum accumulates member
contributions and the sum destroys the decomposition, while
g_ensVoteVoterMask records only WHETHER a member voted, never what.
g_ensVoteMember[] keeps them unsummed. The live arithmetic is untouched.
ENS_MAX_MEMBERS is 8 and deliberately larger than MAX_AI_SIGNALS (5):
independent caps, over-allocating is free, and matching them would make
this array silently short the day the registry grows - a cap that has
already dropped a member once without saying so.
MLPKFoldCVLBFGS. Every baseline row carries a binomial SE, which is the
sampling error of SCORING a fixed model and says nothing about how much
the FIT moves. One LBFGS run from one random start can land anywhere,
and a baseline that cleared or missed the bar on luck of initialisation
reads exactly like one that did it on merit. 3 folds, because each is a
full retrain. LBFGS not LM - LM builds a Hessian over ~7,700 weights.
CCorr ALL-LAGS PROFILE. Added BESIDE the MI lag profile, not instead:
MI catches nonlinear dependence and is the stronger negative, which is
why it settled the verdict - but its per-lag permutation null limits it
to ~20 lags. FFT correlation gets every lag in one O(n log n) pass, so
linear structure parked at lag 300 would surface for free. Different
question, not a replacement. Walks CONTIGUOUS bars, unlike everything
else in this file, because a lag index is meaningless otherwise; both
series are mean-centred first since CorrR1D is a raw sum of products;
and the max over columns x lags is judged against a Sidak family of
exactly that size, not a bare 2-sigma line.
fasttransforms.mqh needed its own include - verified that none of
ap/optimization/statistics/solvers/linalg reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:44:36 -04:00
|
|
|
//--- ...AND AN ERROR BAR ON IT. ReportBaselineModel gives every row a binomial SE, which is the
|
2026-08-22 00:25:52 -04:00
|
|
|
//--- sampling error of SCORING a fixed model. PREDICTED, not attempted. K-fold cross-validation
|
|
|
|
|
//--- measures that second variance directly, on the training rows only, so it never touches the
|
|
|
|
|
//--- OOS window the score below is taken on.
|
fix(baselines): the diagnostic held the EA thread long enough for MetaTrader to kill the process
Run_Alglib_Baselines was described as bounded because it 'cannot outrun the bar it runs on'. That
bound is four hours on H4, it was never enforced, and it is the wrong target: the EA is single
threaded, so while this pass runs there is no training, no panel, no tick handling, and a removal
request only queues.
Measured today on SP500 H4, 800 inputs x 4000 rows: forest 5 s, MLP 52 s, then the 3-fold CV -
three more full retrains. At 15:18:16, 83 s into it, MetaTrader force-terminated the EA. OnDeinit
never ran, so every arrow, panel object and label was orphaned on the chart. The init purge cleans
that up on re-attach; nothing in the pass writes to a cache, saves a model or feeds a decision, so
the damage was the freeze and the dirty chart, not corrupted state.
Three changes:
- BASELINE_MAX_CELLS. Every fit costs O(rows x width) and the row caps carried no width term.
Rows now shrink as the window widens - 1000 instead of 4000 at 800 columns - and the log says
when the cap bit.
- BASELINE_BUDGET_MS with BaselineBudgetSpent(), replacing the bare ShutdownRequested() calls at
each phase boundary. A stop and a spent budget need the same answer and only one was asked.
- The cross-validation is now declined BEFORE it starts, from its predicted cost - the measured MLP
time times the fold count. A boundary check cannot help once a phase is in flight, which is
exactly the phase that was in flight when the process died.
Also: the MLP now reports its weight count against its row count. At 800 inputs it carries ~6,400
weights, and today's run duly printed a training class error of 0.000. That is knowable from the
shape before fitting, so it is stated rather than left to a cross-validation that may not run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:22:42 -04:00
|
|
|
uint cvPredictedMs = mlpMs * BASELINE_MLP_CV_FOLDS;
|
|
|
|
|
uint cvSpentMs = GetTickCount() - t0;
|
|
|
|
|
bool cvFits = (cvSpentMs + cvPredictedMs < BASELINE_BUDGET_MS);
|
|
|
|
|
if(mlpOK && !cvFits)
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + StringFormat(": ALGLIB MLP cross-validation SKIPPED - %d folds x %.1f s = ~%.1f s"
|
fix(baselines): the diagnostic held the EA thread long enough for MetaTrader to kill the process
Run_Alglib_Baselines was described as bounded because it 'cannot outrun the bar it runs on'. That
bound is four hours on H4, it was never enforced, and it is the wrong target: the EA is single
threaded, so while this pass runs there is no training, no panel, no tick handling, and a removal
request only queues.
Measured today on SP500 H4, 800 inputs x 4000 rows: forest 5 s, MLP 52 s, then the 3-fold CV -
three more full retrains. At 15:18:16, 83 s into it, MetaTrader force-terminated the EA. OnDeinit
never ran, so every arrow, panel object and label was orphaned on the chart. The init purge cleans
that up on re-attach; nothing in the pass writes to a cache, saves a model or feeds a decision, so
the damage was the freeze and the dirty chart, not corrupted state.
Three changes:
- BASELINE_MAX_CELLS. Every fit costs O(rows x width) and the row caps carried no width term.
Rows now shrink as the window widens - 1000 instead of 4000 at 800 columns - and the log says
when the cap bit.
- BASELINE_BUDGET_MS with BaselineBudgetSpent(), replacing the bare ShutdownRequested() calls at
each phase boundary. A stop and a spent budget need the same answer and only one was asked.
- The cross-validation is now declined BEFORE it starts, from its predicted cost - the measured MLP
time times the fold count. A boundary check cannot help once a phase is in flight, which is
exactly the phase that was in flight when the process died.
Also: the MLP now reports its weight count against its row count. At 800 inputs it carries ~6,400
weights, and today's run duly printed a training class error of 0.000. That is knowable from the
shape before fitting, so it is stated rather than left to a cross-validation that may not run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:22:42 -04:00
|
|
|
" predicted against %.1f s left of the budget. It is an error bar on the"
|
|
|
|
|
" score below, not the score itself, so the pass continues without it.",
|
|
|
|
|
BASELINE_MLP_CV_FOLDS, mlpMs / 1000.0, cvPredictedMs / 1000.0,
|
|
|
|
|
(BASELINE_BUDGET_MS - cvSpentMs) / 1000.0));
|
|
|
|
|
if(mlpOK && cvFits)
|
feat(baselines): combining-weight fit, MLP cross-validation, all-lags correlation
Three ALGLIB additions, all measurement-only and all under the existing
Run_Alglib_Baselines switch.
MinBLEIC COMBINING WEIGHTS. The live ensemble weights each member by its
own pooled holdout win rate - a defensible prior, but not a fit, and
nothing has ever asked what mixture minimises error on the bars the
members disagreed about. Two individually-mediocre members wrong in
different places can beat one individually better, and a per-member win
rate cannot express that because it never looks at them jointly.
Solved on the simplex (w >= 0, sum w = 1), which is exactly what
MinBLEIC is for. Non-negative because a negative weight asserts "trade
the opposite of this member", a claim ~60 effective observations cannot
support. Least squares on the signed outcome rather than precision:
precision is a STEP function of the threshold that no gradient method
can walk, and optimising a smooth proxy for a step decision is how
c3daded put every operating point 14pp underwater - so the result is
reported in BOTH currencies, the SSE it minimised and the directional
hit rate the mixture would actually have scored against the equal mix.
If the second does not improve, the first is noise.
This needed data that did not exist: g_ensVoteSum accumulates member
contributions and the sum destroys the decomposition, while
g_ensVoteVoterMask records only WHETHER a member voted, never what.
g_ensVoteMember[] keeps them unsummed. The live arithmetic is untouched.
ENS_MAX_MEMBERS is 8 and deliberately larger than MAX_AI_SIGNALS (5):
independent caps, over-allocating is free, and matching them would make
this array silently short the day the registry grows - a cap that has
already dropped a member once without saying so.
MLPKFoldCVLBFGS. Every baseline row carries a binomial SE, which is the
sampling error of SCORING a fixed model and says nothing about how much
the FIT moves. One LBFGS run from one random start can land anywhere,
and a baseline that cleared or missed the bar on luck of initialisation
reads exactly like one that did it on merit. 3 folds, because each is a
full retrain. LBFGS not LM - LM builds a Hessian over ~7,700 weights.
CCorr ALL-LAGS PROFILE. Added BESIDE the MI lag profile, not instead:
MI catches nonlinear dependence and is the stronger negative, which is
why it settled the verdict - but its per-lag permutation null limits it
to ~20 lags. FFT correlation gets every lag in one O(n log n) pass, so
linear structure parked at lag 300 would surface for free. Different
question, not a replacement. Walks CONTIGUOUS bars, unlike everything
else in this file, because a lag index is meaningless otherwise; both
series are mean-centred first since CorrR1D is a raw sum of products;
and the max over columns x lags is judged against a Sidak family of
exactly that size, not a bare 2-sigma line.
fasttransforms.mqh needed its own include - verified that none of
ap/optimization/statistics/solvers/linalg reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:44:36 -04:00
|
|
|
{
|
|
|
|
|
CMultilayerPerceptron cvNet;
|
|
|
|
|
CMLPReport cvTrainRep;
|
|
|
|
|
CMLPCVReport cvRep;
|
|
|
|
|
int cvInfo = 0;
|
|
|
|
|
CMLPBase::MLPCreateC1(width, BASELINE_MLP_HIDDEN, 3, cvNet);
|
|
|
|
|
CMLPTrain::MLPKFoldCVLBFGS(cvNet, xy, nTrain, BASELINE_MLP_DECAY, BASELINE_MLP_RESTARTS,
|
|
|
|
|
BASELINE_MLP_WSTEP, BASELINE_MLP_MAXITS, BASELINE_MLP_CV_FOLDS,
|
|
|
|
|
cvInfo, cvTrainRep, cvRep);
|
|
|
|
|
if(cvInfo > 0)
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + StringFormat(": ALGLIB MLP %d-fold CV - held-out class error %.3f vs %.3f in"
|
feat(baselines): combining-weight fit, MLP cross-validation, all-lags correlation
Three ALGLIB additions, all measurement-only and all under the existing
Run_Alglib_Baselines switch.
MinBLEIC COMBINING WEIGHTS. The live ensemble weights each member by its
own pooled holdout win rate - a defensible prior, but not a fit, and
nothing has ever asked what mixture minimises error on the bars the
members disagreed about. Two individually-mediocre members wrong in
different places can beat one individually better, and a per-member win
rate cannot express that because it never looks at them jointly.
Solved on the simplex (w >= 0, sum w = 1), which is exactly what
MinBLEIC is for. Non-negative because a negative weight asserts "trade
the opposite of this member", a claim ~60 effective observations cannot
support. Least squares on the signed outcome rather than precision:
precision is a STEP function of the threshold that no gradient method
can walk, and optimising a smooth proxy for a step decision is how
c3daded put every operating point 14pp underwater - so the result is
reported in BOTH currencies, the SSE it minimised and the directional
hit rate the mixture would actually have scored against the equal mix.
If the second does not improve, the first is noise.
This needed data that did not exist: g_ensVoteSum accumulates member
contributions and the sum destroys the decomposition, while
g_ensVoteVoterMask records only WHETHER a member voted, never what.
g_ensVoteMember[] keeps them unsummed. The live arithmetic is untouched.
ENS_MAX_MEMBERS is 8 and deliberately larger than MAX_AI_SIGNALS (5):
independent caps, over-allocating is free, and matching them would make
this array silently short the day the registry grows - a cap that has
already dropped a member once without saying so.
MLPKFoldCVLBFGS. Every baseline row carries a binomial SE, which is the
sampling error of SCORING a fixed model and says nothing about how much
the FIT moves. One LBFGS run from one random start can land anywhere,
and a baseline that cleared or missed the bar on luck of initialisation
reads exactly like one that did it on merit. 3 folds, because each is a
full retrain. LBFGS not LM - LM builds a Hessian over ~7,700 weights.
CCorr ALL-LAGS PROFILE. Added BESIDE the MI lag profile, not instead:
MI catches nonlinear dependence and is the stronger negative, which is
why it settled the verdict - but its per-lag permutation null limits it
to ~20 lags. FFT correlation gets every lag in one O(n log n) pass, so
linear structure parked at lag 300 would surface for free. Different
question, not a replacement. Walks CONTIGUOUS bars, unlike everything
else in this file, because a lag index is meaningless otherwise; both
series are mean-centred first since CorrR1D is a raw sum of products;
and the max over columns x lags is judged against a Sidak family of
exactly that size, not a bare 2-sigma line.
fasttransforms.mqh needed its own include - verified that none of
ap/optimization/statistics/solvers/linalg reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:44:36 -04:00
|
|
|
" training (avg CE %.4f vs %.4f). A large gap is the fit memorising"
|
|
|
|
|
" its own rows, and it caps what the OOS row below can mean.",
|
|
|
|
|
BASELINE_MLP_CV_FOLDS, cvRep.m_RelCLSError, mrep.m_RelCLSError,
|
|
|
|
|
cvRep.m_AvgCE, mrep.m_AvgCE));
|
|
|
|
|
else
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + StringFormat(": ALGLIB MLP cross-validation did not run (info %d).", cvInfo));
|
fix(baselines): the diagnostic held the EA thread long enough for MetaTrader to kill the process
Run_Alglib_Baselines was described as bounded because it 'cannot outrun the bar it runs on'. That
bound is four hours on H4, it was never enforced, and it is the wrong target: the EA is single
threaded, so while this pass runs there is no training, no panel, no tick handling, and a removal
request only queues.
Measured today on SP500 H4, 800 inputs x 4000 rows: forest 5 s, MLP 52 s, then the 3-fold CV -
three more full retrains. At 15:18:16, 83 s into it, MetaTrader force-terminated the EA. OnDeinit
never ran, so every arrow, panel object and label was orphaned on the chart. The init purge cleans
that up on re-attach; nothing in the pass writes to a cache, saves a model or feeds a decision, so
the damage was the freeze and the dirty chart, not corrupted state.
Three changes:
- BASELINE_MAX_CELLS. Every fit costs O(rows x width) and the row caps carried no width term.
Rows now shrink as the window widens - 1000 instead of 4000 at 800 columns - and the log says
when the cap bit.
- BASELINE_BUDGET_MS with BaselineBudgetSpent(), replacing the bare ShutdownRequested() calls at
each phase boundary. A stop and a spent budget need the same answer and only one was asked.
- The cross-validation is now declined BEFORE it starts, from its predicted cost - the measured MLP
time times the fold count. A boundary check cannot help once a phase is in flight, which is
exactly the phase that was in flight when the process died.
Also: the MLP now reports its weight count against its row count. At 800 inputs it carries ~6,400
weights, and today's run duly printed a training class error of 0.000. That is knowable from the
shape before fitting, so it is stated rather than left to a cross-validation that may not run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:22:42 -04:00
|
|
|
if(BaselineBudgetSpent(t0, "the linear fit"))
|
feat(baselines): combining-weight fit, MLP cross-validation, all-lags correlation
Three ALGLIB additions, all measurement-only and all under the existing
Run_Alglib_Baselines switch.
MinBLEIC COMBINING WEIGHTS. The live ensemble weights each member by its
own pooled holdout win rate - a defensible prior, but not a fit, and
nothing has ever asked what mixture minimises error on the bars the
members disagreed about. Two individually-mediocre members wrong in
different places can beat one individually better, and a per-member win
rate cannot express that because it never looks at them jointly.
Solved on the simplex (w >= 0, sum w = 1), which is exactly what
MinBLEIC is for. Non-negative because a negative weight asserts "trade
the opposite of this member", a claim ~60 effective observations cannot
support. Least squares on the signed outcome rather than precision:
precision is a STEP function of the threshold that no gradient method
can walk, and optimising a smooth proxy for a step decision is how
c3daded put every operating point 14pp underwater - so the result is
reported in BOTH currencies, the SSE it minimised and the directional
hit rate the mixture would actually have scored against the equal mix.
If the second does not improve, the first is noise.
This needed data that did not exist: g_ensVoteSum accumulates member
contributions and the sum destroys the decomposition, while
g_ensVoteVoterMask records only WHETHER a member voted, never what.
g_ensVoteMember[] keeps them unsummed. The live arithmetic is untouched.
ENS_MAX_MEMBERS is 8 and deliberately larger than MAX_AI_SIGNALS (5):
independent caps, over-allocating is free, and matching them would make
this array silently short the day the registry grows - a cap that has
already dropped a member once without saying so.
MLPKFoldCVLBFGS. Every baseline row carries a binomial SE, which is the
sampling error of SCORING a fixed model and says nothing about how much
the FIT moves. One LBFGS run from one random start can land anywhere,
and a baseline that cleared or missed the bar on luck of initialisation
reads exactly like one that did it on merit. 3 folds, because each is a
full retrain. LBFGS not LM - LM builds a Hessian over ~7,700 weights.
CCorr ALL-LAGS PROFILE. Added BESIDE the MI lag profile, not instead:
MI catches nonlinear dependence and is the stronger negative, which is
why it settled the verdict - but its per-lag permutation null limits it
to ~20 lags. FFT correlation gets every lag in one O(n log n) pass, so
linear structure parked at lag 300 would surface for free. Different
question, not a replacement. Walks CONTIGUOUS bars, unlike everything
else in this file, because a lag index is meaningless otherwise; both
series are mean-centred first since CorrR1D is a raw sum of products;
and the max over columns x lags is judged against a Sidak family of
exactly that size, not a bare 2-sigma line.
fasttransforms.mqh needed its own include - verified that none of
ap/optimization/statistics/solvers/linalg reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:44:36 -04:00
|
|
|
return;
|
|
|
|
|
}
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
//--- LINEAR. Same rows, same columns, target shifted from the class index to a signed direction.
|
2026-08-21 18:12:14 -04:00
|
|
|
double lrPredictedMs = BASELINE_LR_MS_AT_800 * MathPow(width / 800.0, 3.0);
|
|
|
|
|
double lrLeftMs = (double)BASELINE_BUDGET_MS - (double)(GetTickCount() - t0);
|
|
|
|
|
bool lrIdentified = (nTrain > width);
|
|
|
|
|
bool lrAffordable = (lrPredictedMs < lrLeftMs);
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
CLinearModel linear;
|
|
|
|
|
CLRReport lrep;
|
|
|
|
|
int lrInfo = 0;
|
2026-08-21 18:12:14 -04:00
|
|
|
bool linearOK = false;
|
|
|
|
|
if(!lrIdentified || !lrAffordable)
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + StringFormat(": linear baseline SKIPPED - %s. The forest and the MLP below are"
|
2026-08-21 18:12:14 -04:00
|
|
|
" unaffected; only the linear row is missing.",
|
|
|
|
|
!lrIdentified
|
|
|
|
|
? StringFormat("%d rows against %d columns, so the normal equations are"
|
|
|
|
|
" singular and any coefficients returned would be one"
|
|
|
|
|
" arbitrary solution of infinitely many", nTrain, width)
|
|
|
|
|
: StringFormat("~%.0f s predicted at %d columns (cost grows as width^3)"
|
|
|
|
|
" against %.0f s left of the budget", lrPredictedMs / 1000.0,
|
|
|
|
|
width, lrLeftMs / 1000.0)));
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
for(int k = 0; k < nTrain; k++)
|
|
|
|
|
xy.Set(k, width, xy.Get(k, width) - 1.0);
|
|
|
|
|
CLinReg::LRBuild(xy, nTrain, width, lrInfo, linear, lrep);
|
|
|
|
|
linearOK = (lrInfo > 0);
|
|
|
|
|
if(!linearOK)
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + StringFormat(": linear baseline did not fit (Alglib info %d) - forest only below.",
|
2026-08-21 18:12:14 -04:00
|
|
|
lrInfo));
|
|
|
|
|
}
|
fix(baselines): the diagnostic held the EA thread long enough for MetaTrader to kill the process
Run_Alglib_Baselines was described as bounded because it 'cannot outrun the bar it runs on'. That
bound is four hours on H4, it was never enforced, and it is the wrong target: the EA is single
threaded, so while this pass runs there is no training, no panel, no tick handling, and a removal
request only queues.
Measured today on SP500 H4, 800 inputs x 4000 rows: forest 5 s, MLP 52 s, then the 3-fold CV -
three more full retrains. At 15:18:16, 83 s into it, MetaTrader force-terminated the EA. OnDeinit
never ran, so every arrow, panel object and label was orphaned on the chart. The init purge cleans
that up on re-attach; nothing in the pass writes to a cache, saves a model or feeds a decision, so
the damage was the freeze and the dirty chart, not corrupted state.
Three changes:
- BASELINE_MAX_CELLS. Every fit costs O(rows x width) and the row caps carried no width term.
Rows now shrink as the window widens - 1000 instead of 4000 at 800 columns - and the log says
when the cap bit.
- BASELINE_BUDGET_MS with BaselineBudgetSpent(), replacing the bare ShutdownRequested() calls at
each phase boundary. A stop and a spent budget need the same answer and only one was asked.
- The cross-validation is now declined BEFORE it starts, from its predicted cost - the measured MLP
time times the fold count. A boundary check cannot help once a phase is in flight, which is
exactly the phase that was in flight when the process died.
Also: the MLP now reports its weight count against its row count. At 800 inputs it carries ~6,400
weights, and today's run duly printed a training class error of 0.000. That is knowable from the
shape before fitting, so it is stated rather than left to a cross-validation that may not run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:22:42 -04:00
|
|
|
if(BaselineBudgetSpent(t0, "the OOS scoring"))
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
return;
|
|
|
|
|
//--- SCORE BOTH ON THE OOS SLICE, in the deploy gate's own currency: a call is a directional
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
//--- prediction, a hit is that direction WINNING at the measured geometry (the view's Outcome),
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
//--- and the benchmark is the always-call-one-direction win rate over the same bars. See the
|
|
|
|
|
//--- chancePrecPct derivation in Train() for why that benchmark and not the label base rate.
|
|
|
|
|
int scored = 0, winLongN = 0, winShortN = 0;
|
feat(baselines): ALGLIB's MLP as a third baseline, not a replacement
Asked whether ALGLIB's perceptron should replace ours "only if it is
better". This measures that instead of assuming it, and the measurement
is worth more than the swap would have been.
The forest and the linear fit test whether the MATRIX carries direction.
Neither tests whether OUR CODE is what fails. ALGLIB's MLP does: it is a
long-standing independent implementation of roughly what our own dense
stack computes, trained on the identical rows through the identical
gate. That distinction is not hypothetical here - two silent
implementation faults have already invalidated every model-based
negative behind them (a transposed dense hidden gradient, so backprop
was never backprop; and Adam storing sqrt(v) and feeding it back as
variance, so every "Adam" run was SGD). If alglib-mlp clears a bar our
own net cannot on the same rows, the fault is in our code.
Trained BEFORE the target column is rewritten for the linear fit:
MLPCreateC1 builds a softmax classifier whose trainer wants the class
index, which is what the matrix still holds at that point. Scored by
argmax with Neutral abstaining, exactly as the forest is. Sidak family
is now 3, not 2.
Deliberately NOT a replacement, and the gap is not close: ALGLIB's MLP
is feed-forward only (no conv, no recurrence - so it could not stand in
for three of the four members at all), and it has no batch norm, no
logit-adjusted loss for the class imbalance, no era resumption, no
OpenCL or CPU-DLL backend and no .nnw. Adopting it would trade a
maintained architecture for a control.
Kept small on purpose - 8 hidden units, 1 restart, 100 iterations, decay
at the 0.001 the ALGLIB docs recommend when you have no reason to pick
another. A wide net would spend the six-core box's hours answering a
question about capacity when the question is about implementation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:19:53 -04:00
|
|
|
int fCalls = 0, fHits = 0, lCalls = 0, lHits = 0, mCalls = 0, mHits = 0;
|
|
|
|
|
double y[], my[];
|
feat(baselines): matrix redundancy and a declustered regime test
Two ALGLIB diagnostics on the pass that already builds the matrix, under
the same Run_Alglib_Baselines switch, so they cost nothing by default
and share one walk of the feature pipeline.
REDUNDANCY (CBaseStat::PearsonCorrM + CPCAnalysis::PCABuildBasis).
The alt-data block already produced this finding once, by hand: 13
features x 16 window slots = 208 inputs spanning ~11.5 effective
dimensions, and the cost was not wasted parameters but a gradient
weighting bias, since batch norm rescales collinear copies without
decorrelating them. This generalises that measurement to every column
instead of the one block someone thought to check.
Run on the ANCHOR BAR'S ROW, not the full window. The window is
m_historyBars near-copies of the same columns at different lags, so
per-bar redundancy is the question worth asking - the lag structure is
what the conv/LSTM stage exists to exploit - and it keeps the eigensolve
at ~60x60 rather than ~960x960.
REGIME STABILITY (CMannWhitneyU::CMannWhitneyUTest) on the net's own
signed margin: its conviction, signed by whether the conviction paid,
older half of the OOS window against newer.
DECLUSTERED FIRST, which is the whole reason this is not a one-line
call. Triple-barrier labels overlap, so consecutive scored bars share
the price action that resolved them and are not independent draws.
Feeding all of them to a rank test yields a p computed on an n it does
not have - the same error label overlap invalidated everywhere else
here. One sample per label horizon costs most of the rows and buys a p
that means something; when too few survive, it says so and declines.
Both report and neither acts. Wiring the regime flag to position sizing
is the obvious next step and the one to take only after watching the
number: run every era on ~60 effective observations, a 0.05 test crosses
by chance regularly, and an auto-derisk on that is a random position
size generator.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:01:34 -04:00
|
|
|
//--- THE NET'S OWN per-bar outcome, collected on the same walk for the regime test below. Paired
|
|
|
|
|
//--- with the bar index so the test can decluster before it computes a p-value.
|
|
|
|
|
double margins[];
|
|
|
|
|
int marginBars[];
|
|
|
|
|
ArrayResize(margins, 0);
|
|
|
|
|
ArrayResize(marginBars, 0);
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
for(int k = 0; k < nScoreWanted; k++)
|
|
|
|
|
{
|
|
|
|
|
int r = scoreBars[k];
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
if(!m_data.RowFeatures(r, width, x))
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
continue;
|
|
|
|
|
scored++;
|
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
|
|
|
bool wl = m_data.IsBuyLabel(r), ws = m_data.IsSellLabel(r);
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
if(wl) winLongN++;
|
|
|
|
|
if(ws) winShortN++;
|
feat(baselines): matrix redundancy and a declustered regime test
Two ALGLIB diagnostics on the pass that already builds the matrix, under
the same Run_Alglib_Baselines switch, so they cost nothing by default
and share one walk of the feature pipeline.
REDUNDANCY (CBaseStat::PearsonCorrM + CPCAnalysis::PCABuildBasis).
The alt-data block already produced this finding once, by hand: 13
features x 16 window slots = 208 inputs spanning ~11.5 effective
dimensions, and the cost was not wasted parameters but a gradient
weighting bias, since batch norm rescales collinear copies without
decorrelating them. This generalises that measurement to every column
instead of the one block someone thought to check.
Run on the ANCHOR BAR'S ROW, not the full window. The window is
m_historyBars near-copies of the same columns at different lags, so
per-bar redundancy is the question worth asking - the lag structure is
what the conv/LSTM stage exists to exploit - and it keeps the eigensolve
at ~60x60 rather than ~960x960.
REGIME STABILITY (CMannWhitneyU::CMannWhitneyUTest) on the net's own
signed margin: its conviction, signed by whether the conviction paid,
older half of the OOS window against newer.
DECLUSTERED FIRST, which is the whole reason this is not a one-line
call. Triple-barrier labels overlap, so consecutive scored bars share
the price action that resolved them and are not independent draws.
Feeding all of them to a rank test yields a p computed on an n it does
not have - the same error label overlap invalidated everywhere else
here. One sample per label horizon costs most of the rows and buys a p
that means something; when too few survive, it says so and declines.
Both report and neither acts. Wiring the regime flag to position sizing
is the obvious next step and the one to take only after watching the
number: run every era on ~60 effective observations, a 0.05 test crosses
by chance regularly, and an auto-derisk on that is a random position
size generator.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:01:34 -04:00
|
|
|
//--- The arrow cache holds pass 3's ADJUSTED decision per scored bar, and pass 3 has just
|
|
|
|
|
//--- finished, so it is complete for this era at exactly this moment (it is wiped at the NEXT
|
2026-08-22 00:25:52 -04:00
|
|
|
//--- era start).
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
bool calledBuy = false;
|
|
|
|
|
double calledMag = 0.0;
|
|
|
|
|
if(m_data.DirectionalCall(r, calledBuy, calledMag))
|
feat(baselines): matrix redundancy and a declustered regime test
Two ALGLIB diagnostics on the pass that already builds the matrix, under
the same Run_Alglib_Baselines switch, so they cost nothing by default
and share one walk of the feature pipeline.
REDUNDANCY (CBaseStat::PearsonCorrM + CPCAnalysis::PCABuildBasis).
The alt-data block already produced this finding once, by hand: 13
features x 16 window slots = 208 inputs spanning ~11.5 effective
dimensions, and the cost was not wasted parameters but a gradient
weighting bias, since batch norm rescales collinear copies without
decorrelating them. This generalises that measurement to every column
instead of the one block someone thought to check.
Run on the ANCHOR BAR'S ROW, not the full window. The window is
m_historyBars near-copies of the same columns at different lags, so
per-bar redundancy is the question worth asking - the lag structure is
what the conv/LSTM stage exists to exploit - and it keeps the eigensolve
at ~60x60 rather than ~960x960.
REGIME STABILITY (CMannWhitneyU::CMannWhitneyUTest) on the net's own
signed margin: its conviction, signed by whether the conviction paid,
older half of the OOS window against newer.
DECLUSTERED FIRST, which is the whole reason this is not a one-line
call. Triple-barrier labels overlap, so consecutive scored bars share
the price action that resolved them and are not independent draws.
Feeding all of them to a rank test yields a p computed on an n it does
not have - the same error label overlap invalidated everywhere else
here. One sample per label horizon costs most of the rows and buys a p
that means something; when too few survive, it says so and declines.
Both report and neither acts. Wiring the regime flag to position sizing
is the obvious next step and the one to take only after watching the
number: run every era on ~60 effective observations, a 0.05 test crosses
by chance regularly, and an auto-derisk on that is a random position
size generator.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:01:34 -04:00
|
|
|
{
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
int mi = ArraySize(margins);
|
|
|
|
|
ArrayResize(margins, mi + 1);
|
|
|
|
|
ArrayResize(marginBars, mi + 1);
|
|
|
|
|
//--- SIGNED BY WHETHER IT WAS RIGHT, not by which way it pointed: the regime test asks
|
|
|
|
|
//--- whether the model's confidence still tracks its outcomes, so a confident loser has
|
|
|
|
|
//--- to score negative.
|
|
|
|
|
margins[mi] = calledMag * ((calledBuy ? wl : ws) ? 1.0 : -1.0);
|
|
|
|
|
marginBars[mi] = r;
|
feat(baselines): matrix redundancy and a declustered regime test
Two ALGLIB diagnostics on the pass that already builds the matrix, under
the same Run_Alglib_Baselines switch, so they cost nothing by default
and share one walk of the feature pipeline.
REDUNDANCY (CBaseStat::PearsonCorrM + CPCAnalysis::PCABuildBasis).
The alt-data block already produced this finding once, by hand: 13
features x 16 window slots = 208 inputs spanning ~11.5 effective
dimensions, and the cost was not wasted parameters but a gradient
weighting bias, since batch norm rescales collinear copies without
decorrelating them. This generalises that measurement to every column
instead of the one block someone thought to check.
Run on the ANCHOR BAR'S ROW, not the full window. The window is
m_historyBars near-copies of the same columns at different lags, so
per-bar redundancy is the question worth asking - the lag structure is
what the conv/LSTM stage exists to exploit - and it keeps the eigensolve
at ~60x60 rather than ~960x960.
REGIME STABILITY (CMannWhitneyU::CMannWhitneyUTest) on the net's own
signed margin: its conviction, signed by whether the conviction paid,
older half of the OOS window against newer.
DECLUSTERED FIRST, which is the whole reason this is not a one-line
call. Triple-barrier labels overlap, so consecutive scored bars share
the price action that resolved them and are not independent draws.
Feeding all of them to a rank test yields a p computed on an n it does
not have - the same error label overlap invalidated everywhere else
here. One sample per label horizon costs most of the rows and buys a p
that means something; when too few survive, it says so and declines.
Both report and neither acts. Wiring the regime flag to position sizing
is the obvious next step and the one to take only after watching the
number: run every era on ~60 effective observations, a 0.05 test crosses
by chance regularly, and an auto-derisk on that is a random position
size generator.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:01:34 -04:00
|
|
|
}
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
//--- Forest: argmax over the three class probabilities. A Neutral argmax is an abstention and
|
|
|
|
|
//--- enters neither the numerator nor the denominator, exactly as the net's own Neutral does.
|
|
|
|
|
CDForest::DFProcess(forest, x, y);
|
|
|
|
|
if(ArraySize(y) == 3)
|
|
|
|
|
{
|
|
|
|
|
int arg = 0;
|
|
|
|
|
for(int c = 1; c < 3; c++)
|
|
|
|
|
if(y[c] > y[arg])
|
|
|
|
|
arg = c;
|
|
|
|
|
if(arg != 1) // 1 == Neutral == abstain
|
|
|
|
|
{
|
|
|
|
|
fCalls++;
|
|
|
|
|
if(arg == 2 ? wl : ws)
|
|
|
|
|
fHits++;
|
|
|
|
|
}
|
|
|
|
|
}
|
feat(baselines): ALGLIB's MLP as a third baseline, not a replacement
Asked whether ALGLIB's perceptron should replace ours "only if it is
better". This measures that instead of assuming it, and the measurement
is worth more than the swap would have been.
The forest and the linear fit test whether the MATRIX carries direction.
Neither tests whether OUR CODE is what fails. ALGLIB's MLP does: it is a
long-standing independent implementation of roughly what our own dense
stack computes, trained on the identical rows through the identical
gate. That distinction is not hypothetical here - two silent
implementation faults have already invalidated every model-based
negative behind them (a transposed dense hidden gradient, so backprop
was never backprop; and Adam storing sqrt(v) and feeding it back as
variance, so every "Adam" run was SGD). If alglib-mlp clears a bar our
own net cannot on the same rows, the fault is in our code.
Trained BEFORE the target column is rewritten for the linear fit:
MLPCreateC1 builds a softmax classifier whose trainer wants the class
index, which is what the matrix still holds at that point. Scored by
argmax with Neutral abstaining, exactly as the forest is. Sidak family
is now 3, not 2.
Deliberately NOT a replacement, and the gap is not close: ALGLIB's MLP
is feed-forward only (no conv, no recurrence - so it could not stand in
for three of the four members at all), and it has no batch norm, no
logit-adjusted loss for the class imbalance, no era resumption, no
OpenCL or CPU-DLL backend and no .nnw. Adopting it would trade a
maintained architecture for a control.
Kept small on purpose - 8 hidden units, 1 restart, 100 iterations, decay
at the 0.001 the ALGLIB docs recommend when you have no reason to pick
another. A wide net would spend the six-core box's hours answering a
question about capacity when the question is about implementation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:19:53 -04:00
|
|
|
//--- MLP: argmax over the softmax outputs, Neutral abstains - scored exactly as the forest is.
|
|
|
|
|
if(mlpOK)
|
|
|
|
|
{
|
|
|
|
|
CMLPBase::MLPProcess(mlp, x, my);
|
|
|
|
|
if(ArraySize(my) == 3)
|
|
|
|
|
{
|
|
|
|
|
int marg = 0;
|
|
|
|
|
for(int c = 1; c < 3; c++)
|
|
|
|
|
if(my[c] > my[marg])
|
|
|
|
|
marg = c;
|
|
|
|
|
if(marg != 1)
|
|
|
|
|
{
|
|
|
|
|
mCalls++;
|
|
|
|
|
if(marg == 2 ? wl : ws)
|
|
|
|
|
mHits++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
//--- Linear: the sign of the fitted value. No threshold sweep - a threshold fitted on this same
|
|
|
|
|
//--- slice is the calibration leak this codebase carved a purged band to avoid, and a baseline
|
|
|
|
|
//--- allowed one operating point per candidate would be the best-of-N problem all over again.
|
|
|
|
|
if(linearOK)
|
|
|
|
|
{
|
|
|
|
|
double pred = CLinReg::LRProcess(linear, x);
|
|
|
|
|
if(MathIsValidNumber(pred) && pred != 0.0)
|
|
|
|
|
{
|
|
|
|
|
lCalls++;
|
|
|
|
|
bool won = (pred > 0.0) ? wl : ws;
|
|
|
|
|
if(won)
|
|
|
|
|
lHits++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if(scored < BASELINE_MIN_ROWS)
|
|
|
|
|
{
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + StringFormat(": Alglib baselines scored only %d OOS windows - nothing to report.",
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
scored));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
double chancePct = 100.0 * MathMax(winLongN, winShortN) / scored;
|
|
|
|
|
ReportBaselineModel("forest", fCalls, fHits, scored, chancePct);
|
feat(baselines): ALGLIB's MLP as a third baseline, not a replacement
Asked whether ALGLIB's perceptron should replace ours "only if it is
better". This measures that instead of assuming it, and the measurement
is worth more than the swap would have been.
The forest and the linear fit test whether the MATRIX carries direction.
Neither tests whether OUR CODE is what fails. ALGLIB's MLP does: it is a
long-standing independent implementation of roughly what our own dense
stack computes, trained on the identical rows through the identical
gate. That distinction is not hypothetical here - two silent
implementation faults have already invalidated every model-based
negative behind them (a transposed dense hidden gradient, so backprop
was never backprop; and Adam storing sqrt(v) and feeding it back as
variance, so every "Adam" run was SGD). If alglib-mlp clears a bar our
own net cannot on the same rows, the fault is in our code.
Trained BEFORE the target column is rewritten for the linear fit:
MLPCreateC1 builds a softmax classifier whose trainer wants the class
index, which is what the matrix still holds at that point. Scored by
argmax with Neutral abstaining, exactly as the forest is. Sidak family
is now 3, not 2.
Deliberately NOT a replacement, and the gap is not close: ALGLIB's MLP
is feed-forward only (no conv, no recurrence - so it could not stand in
for three of the four members at all), and it has no batch norm, no
logit-adjusted loss for the class imbalance, no era resumption, no
OpenCL or CPU-DLL backend and no .nnw. Adopting it would trade a
maintained architecture for a control.
Kept small on purpose - 8 hidden units, 1 restart, 100 iterations, decay
at the 0.001 the ALGLIB docs recommend when you have no reason to pick
another. A wide net would spend the six-core box's hours answering a
question about capacity when the question is about implementation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:19:53 -04:00
|
|
|
if(mlpOK)
|
|
|
|
|
ReportBaselineModel("alglib-mlp", mCalls, mHits, scored, chancePct);
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
if(linearOK)
|
|
|
|
|
ReportBaselineModel("linear", lCalls, lHits, scored, chancePct);
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
double gatePrec = -1.0, gateChance = -1.0;
|
|
|
|
|
int gateCalls = 0;
|
|
|
|
|
m_data.GateReference(gatePrec, gateCalls, gateChance);
|
|
|
|
|
Print(m_data.Id() + StringFormat(": baseline reference - the net's own gate on this chart last read"
|
feat(baselines): ALGLIB's MLP as a third baseline, not a replacement
Asked whether ALGLIB's perceptron should replace ours "only if it is
better". This measures that instead of assuming it, and the measurement
is worth more than the swap would have been.
The forest and the linear fit test whether the MATRIX carries direction.
Neither tests whether OUR CODE is what fails. ALGLIB's MLP does: it is a
long-standing independent implementation of roughly what our own dense
stack computes, trained on the identical rows through the identical
gate. That distinction is not hypothetical here - two silent
implementation faults have already invalidated every model-based
negative behind them (a transposed dense hidden gradient, so backprop
was never backprop; and Adam storing sqrt(v) and feeding it back as
variance, so every "Adam" run was SGD). If alglib-mlp clears a bar our
own net cannot on the same rows, the fault is in our code.
Trained BEFORE the target column is rewritten for the linear fit:
MLPCreateC1 builds a softmax classifier whose trainer wants the class
index, which is what the matrix still holds at that point. Scored by
argmax with Neutral abstaining, exactly as the forest is. Sidak family
is now 3, not 2.
Deliberately NOT a replacement, and the gap is not close: ALGLIB's MLP
is feed-forward only (no conv, no recurrence - so it could not stand in
for three of the four members at all), and it has no batch norm, no
logit-adjusted loss for the class imbalance, no era resumption, no
OpenCL or CPU-DLL backend and no .nnw. Adopting it would trade a
maintained architecture for a control.
Kept small on purpose - 8 hidden units, 1 restart, 100 iterations, decay
at the 0.001 the ALGLIB docs recommend when you have no reason to pick
another. A wide net would spend the six-core box's hours answering a
question about capacity when the question is about implementation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:19:53 -04:00
|
|
|
" %.1f%% precision on %d calls against %.1f%% chance. Read the rows above"
|
|
|
|
|
" against it: all at chance means the MATRIX is the limit, not the topology;"
|
|
|
|
|
" alglib-mlp well ABOVE our own net on the same rows would point at our"
|
|
|
|
|
" IMPLEMENTATION rather than at the data.",
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
gatePrec, gateCalls, gateChance));
|
fix(baselines): the diagnostic held the EA thread long enough for MetaTrader to kill the process
Run_Alglib_Baselines was described as bounded because it 'cannot outrun the bar it runs on'. That
bound is four hours on H4, it was never enforced, and it is the wrong target: the EA is single
threaded, so while this pass runs there is no training, no panel, no tick handling, and a removal
request only queues.
Measured today on SP500 H4, 800 inputs x 4000 rows: forest 5 s, MLP 52 s, then the 3-fold CV -
three more full retrains. At 15:18:16, 83 s into it, MetaTrader force-terminated the EA. OnDeinit
never ran, so every arrow, panel object and label was orphaned on the chart. The init purge cleans
that up on re-attach; nothing in the pass writes to a cache, saves a model or feeds a decision, so
the damage was the freeze and the dirty chart, not corrupted state.
Three changes:
- BASELINE_MAX_CELLS. Every fit costs O(rows x width) and the row caps carried no width term.
Rows now shrink as the window widens - 1000 instead of 4000 at 800 columns - and the log says
when the cap bit.
- BASELINE_BUDGET_MS with BaselineBudgetSpent(), replacing the bare ShutdownRequested() calls at
each phase boundary. A stop and a spent budget need the same answer and only one was asked.
- The cross-validation is now declined BEFORE it starts, from its predicted cost - the measured MLP
time times the fold count. A boundary check cannot help once a phase is in flight, which is
exactly the phase that was in flight when the process died.
Also: the MLP now reports its weight count against its row count. At 800 inputs it carries ~6,400
weights, and today's run duly printed a training class error of 0.000. That is knowable from the
shape before fitting, so it is stated rather than left to a cross-validation that may not run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:22:42 -04:00
|
|
|
if(BaselineBudgetSpent(t0, "the redundancy / regime / lag reports"))
|
feat(baselines): matrix redundancy and a declustered regime test
Two ALGLIB diagnostics on the pass that already builds the matrix, under
the same Run_Alglib_Baselines switch, so they cost nothing by default
and share one walk of the feature pipeline.
REDUNDANCY (CBaseStat::PearsonCorrM + CPCAnalysis::PCABuildBasis).
The alt-data block already produced this finding once, by hand: 13
features x 16 window slots = 208 inputs spanning ~11.5 effective
dimensions, and the cost was not wasted parameters but a gradient
weighting bias, since batch norm rescales collinear copies without
decorrelating them. This generalises that measurement to every column
instead of the one block someone thought to check.
Run on the ANCHOR BAR'S ROW, not the full window. The window is
m_historyBars near-copies of the same columns at different lags, so
per-bar redundancy is the question worth asking - the lag structure is
what the conv/LSTM stage exists to exploit - and it keeps the eigensolve
at ~60x60 rather than ~960x960.
REGIME STABILITY (CMannWhitneyU::CMannWhitneyUTest) on the net's own
signed margin: its conviction, signed by whether the conviction paid,
older half of the OOS window against newer.
DECLUSTERED FIRST, which is the whole reason this is not a one-line
call. Triple-barrier labels overlap, so consecutive scored bars share
the price action that resolved them and are not independent draws.
Feeding all of them to a rank test yields a p computed on an n it does
not have - the same error label overlap invalidated everywhere else
here. One sample per label horizon costs most of the rows and buys a p
that means something; when too few survive, it says so and declines.
Both report and neither acts. Wiring the regime flag to position sizing
is the obvious next step and the one to take only after watching the
number: run every era on ~60 effective observations, a 0.05 test crosses
by chance regularly, and an auto-derisk on that is a random position
size generator.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:01:34 -04:00
|
|
|
return;
|
|
|
|
|
ReportFeatureRedundancy(anchors, nTrain);
|
|
|
|
|
ReportRegimeStability(margins, marginBars);
|
feat(baselines): combining-weight fit, MLP cross-validation, all-lags correlation
Three ALGLIB additions, all measurement-only and all under the existing
Run_Alglib_Baselines switch.
MinBLEIC COMBINING WEIGHTS. The live ensemble weights each member by its
own pooled holdout win rate - a defensible prior, but not a fit, and
nothing has ever asked what mixture minimises error on the bars the
members disagreed about. Two individually-mediocre members wrong in
different places can beat one individually better, and a per-member win
rate cannot express that because it never looks at them jointly.
Solved on the simplex (w >= 0, sum w = 1), which is exactly what
MinBLEIC is for. Non-negative because a negative weight asserts "trade
the opposite of this member", a claim ~60 effective observations cannot
support. Least squares on the signed outcome rather than precision:
precision is a STEP function of the threshold that no gradient method
can walk, and optimising a smooth proxy for a step decision is how
c3daded put every operating point 14pp underwater - so the result is
reported in BOTH currencies, the SSE it minimised and the directional
hit rate the mixture would actually have scored against the equal mix.
If the second does not improve, the first is noise.
This needed data that did not exist: g_ensVoteSum accumulates member
contributions and the sum destroys the decomposition, while
g_ensVoteVoterMask records only WHETHER a member voted, never what.
g_ensVoteMember[] keeps them unsummed. The live arithmetic is untouched.
ENS_MAX_MEMBERS is 8 and deliberately larger than MAX_AI_SIGNALS (5):
independent caps, over-allocating is free, and matching them would make
this array silently short the day the registry grows - a cap that has
already dropped a member once without saying so.
MLPKFoldCVLBFGS. Every baseline row carries a binomial SE, which is the
sampling error of SCORING a fixed model and says nothing about how much
the FIT moves. One LBFGS run from one random start can land anywhere,
and a baseline that cleared or missed the bar on luck of initialisation
reads exactly like one that did it on merit. 3 folds, because each is a
full retrain. LBFGS not LM - LM builds a Hessian over ~7,700 weights.
CCorr ALL-LAGS PROFILE. Added BESIDE the MI lag profile, not instead:
MI catches nonlinear dependence and is the stronger negative, which is
why it settled the verdict - but its per-lag permutation null limits it
to ~20 lags. FFT correlation gets every lag in one O(n log n) pass, so
linear structure parked at lag 300 would surface for free. Different
question, not a replacement. Walks CONTIGUOUS bars, unlike everything
else in this file, because a lag index is meaningless otherwise; both
series are mean-centred first since CorrR1D is a raw sum of products;
and the max over columns x lags is judged against a Sidak family of
exactly that size, not a bare 2-sigma line.
fasttransforms.mqh needed its own include - verified that none of
ap/optimization/statistics/solvers/linalg reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:44:36 -04:00
|
|
|
ReportLinearLagProfile(bars, oosCutoff);
|
|
|
|
|
ReportCombiningWeights();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
2026-08-22 00:30:14 -04:00
|
|
|
//| WHAT WOULD THE BEST MIX OF MEMBERS HAVE BEEN? |
|
feat(baselines): combining-weight fit, MLP cross-validation, all-lags correlation
Three ALGLIB additions, all measurement-only and all under the existing
Run_Alglib_Baselines switch.
MinBLEIC COMBINING WEIGHTS. The live ensemble weights each member by its
own pooled holdout win rate - a defensible prior, but not a fit, and
nothing has ever asked what mixture minimises error on the bars the
members disagreed about. Two individually-mediocre members wrong in
different places can beat one individually better, and a per-member win
rate cannot express that because it never looks at them jointly.
Solved on the simplex (w >= 0, sum w = 1), which is exactly what
MinBLEIC is for. Non-negative because a negative weight asserts "trade
the opposite of this member", a claim ~60 effective observations cannot
support. Least squares on the signed outcome rather than precision:
precision is a STEP function of the threshold that no gradient method
can walk, and optimising a smooth proxy for a step decision is how
c3daded put every operating point 14pp underwater - so the result is
reported in BOTH currencies, the SSE it minimised and the directional
hit rate the mixture would actually have scored against the equal mix.
If the second does not improve, the first is noise.
This needed data that did not exist: g_ensVoteSum accumulates member
contributions and the sum destroys the decomposition, while
g_ensVoteVoterMask records only WHETHER a member voted, never what.
g_ensVoteMember[] keeps them unsummed. The live arithmetic is untouched.
ENS_MAX_MEMBERS is 8 and deliberately larger than MAX_AI_SIGNALS (5):
independent caps, over-allocating is free, and matching them would make
this array silently short the day the registry grows - a cap that has
already dropped a member once without saying so.
MLPKFoldCVLBFGS. Every baseline row carries a binomial SE, which is the
sampling error of SCORING a fixed model and says nothing about how much
the FIT moves. One LBFGS run from one random start can land anywhere,
and a baseline that cleared or missed the bar on luck of initialisation
reads exactly like one that did it on merit. 3 folds, because each is a
full retrain. LBFGS not LM - LM builds a Hessian over ~7,700 weights.
CCorr ALL-LAGS PROFILE. Added BESIDE the MI lag profile, not instead:
MI catches nonlinear dependence and is the stronger negative, which is
why it settled the verdict - but its per-lag permutation null limits it
to ~20 lags. FFT correlation gets every lag in one O(n log n) pass, so
linear structure parked at lag 300 would surface for free. Different
question, not a replacement. Walks CONTIGUOUS bars, unlike everything
else in this file, because a lag index is meaningless otherwise; both
series are mean-centred first since CorrR1D is a raw sum of products;
and the max over columns x lags is judged against a Sidak family of
exactly that size, not a bare 2-sigma line.
fasttransforms.mqh needed its own include - verified that none of
ap/optimization/statistics/solvers/linalg reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:44:36 -04:00
|
|
|
//+------------------------------------------------------------------+
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
void CBaselineComparator::ReportCombiningWeights(void)
|
feat(baselines): combining-weight fit, MLP cross-validation, all-lags correlation
Three ALGLIB additions, all measurement-only and all under the existing
Run_Alglib_Baselines switch.
MinBLEIC COMBINING WEIGHTS. The live ensemble weights each member by its
own pooled holdout win rate - a defensible prior, but not a fit, and
nothing has ever asked what mixture minimises error on the bars the
members disagreed about. Two individually-mediocre members wrong in
different places can beat one individually better, and a per-member win
rate cannot express that because it never looks at them jointly.
Solved on the simplex (w >= 0, sum w = 1), which is exactly what
MinBLEIC is for. Non-negative because a negative weight asserts "trade
the opposite of this member", a claim ~60 effective observations cannot
support. Least squares on the signed outcome rather than precision:
precision is a STEP function of the threshold that no gradient method
can walk, and optimising a smooth proxy for a step decision is how
c3daded put every operating point 14pp underwater - so the result is
reported in BOTH currencies, the SSE it minimised and the directional
hit rate the mixture would actually have scored against the equal mix.
If the second does not improve, the first is noise.
This needed data that did not exist: g_ensVoteSum accumulates member
contributions and the sum destroys the decomposition, while
g_ensVoteVoterMask records only WHETHER a member voted, never what.
g_ensVoteMember[] keeps them unsummed. The live arithmetic is untouched.
ENS_MAX_MEMBERS is 8 and deliberately larger than MAX_AI_SIGNALS (5):
independent caps, over-allocating is free, and matching them would make
this array silently short the day the registry grows - a cap that has
already dropped a member once without saying so.
MLPKFoldCVLBFGS. Every baseline row carries a binomial SE, which is the
sampling error of SCORING a fixed model and says nothing about how much
the FIT moves. One LBFGS run from one random start can land anywhere,
and a baseline that cleared or missed the bar on luck of initialisation
reads exactly like one that did it on merit. 3 folds, because each is a
full retrain. LBFGS not LM - LM builds a Hessian over ~7,700 weights.
CCorr ALL-LAGS PROFILE. Added BESIDE the MI lag profile, not instead:
MI catches nonlinear dependence and is the stronger negative, which is
why it settled the verdict - but its per-lag permutation null limits it
to ~20 lags. FFT correlation gets every lag in one O(n log n) pass, so
linear structure parked at lag 300 would surface for free. Different
question, not a replacement. Walks CONTIGUOUS bars, unlike everything
else in this file, because a lag index is meaningless otherwise; both
series are mean-centred first since CorrR1D is a raw sum of products;
and the max over columns x lags is judged against a Sidak family of
exactly that size, not a bare 2-sigma line.
fasttransforms.mqh needed its own include - verified that none of
ap/optimization/statistics/solvers/linalg reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:44:36 -04:00
|
|
|
{
|
|
|
|
|
//--- ONE MEMBER RUNS THIS. The vote rows are ensemble-global, so every member would otherwise fit
|
|
|
|
|
//--- and print the identical answer once each.
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
if(!m_data.IsEnsembleMember() || m_data.EnsembleIndex() != 0 || g_ensVoteRows < BASELINE_MIN_ROWS)
|
feat(baselines): combining-weight fit, MLP cross-validation, all-lags correlation
Three ALGLIB additions, all measurement-only and all under the existing
Run_Alglib_Baselines switch.
MinBLEIC COMBINING WEIGHTS. The live ensemble weights each member by its
own pooled holdout win rate - a defensible prior, but not a fit, and
nothing has ever asked what mixture minimises error on the bars the
members disagreed about. Two individually-mediocre members wrong in
different places can beat one individually better, and a per-member win
rate cannot express that because it never looks at them jointly.
Solved on the simplex (w >= 0, sum w = 1), which is exactly what
MinBLEIC is for. Non-negative because a negative weight asserts "trade
the opposite of this member", a claim ~60 effective observations cannot
support. Least squares on the signed outcome rather than precision:
precision is a STEP function of the threshold that no gradient method
can walk, and optimising a smooth proxy for a step decision is how
c3daded put every operating point 14pp underwater - so the result is
reported in BOTH currencies, the SSE it minimised and the directional
hit rate the mixture would actually have scored against the equal mix.
If the second does not improve, the first is noise.
This needed data that did not exist: g_ensVoteSum accumulates member
contributions and the sum destroys the decomposition, while
g_ensVoteVoterMask records only WHETHER a member voted, never what.
g_ensVoteMember[] keeps them unsummed. The live arithmetic is untouched.
ENS_MAX_MEMBERS is 8 and deliberately larger than MAX_AI_SIGNALS (5):
independent caps, over-allocating is free, and matching them would make
this array silently short the day the registry grows - a cap that has
already dropped a member once without saying so.
MLPKFoldCVLBFGS. Every baseline row carries a binomial SE, which is the
sampling error of SCORING a fixed model and says nothing about how much
the FIT moves. One LBFGS run from one random start can land anywhere,
and a baseline that cleared or missed the bar on luck of initialisation
reads exactly like one that did it on merit. 3 folds, because each is a
full retrain. LBFGS not LM - LM builds a Hessian over ~7,700 weights.
CCorr ALL-LAGS PROFILE. Added BESIDE the MI lag profile, not instead:
MI catches nonlinear dependence and is the stronger negative, which is
why it settled the verdict - but its per-lag permutation null limits it
to ~20 lags. FFT correlation gets every lag in one O(n log n) pass, so
linear structure parked at lag 300 would surface for free. Different
question, not a replacement. Walks CONTIGUOUS bars, unlike everything
else in this file, because a lag index is meaningless otherwise; both
series are mean-centred first since CorrR1D is a raw sum of products;
and the max over columns x lags is judged against a Sidak family of
exactly that size, not a bare 2-sigma line.
fasttransforms.mqh needed its own include - verified that none of
ap/optimization/statistics/solvers/linalg reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:44:36 -04:00
|
|
|
return;
|
|
|
|
|
//--- Which slots actually carry a member this run - the registry can be sparse.
|
|
|
|
|
int slot[ENS_MAX_MEMBERS], members = 0;
|
|
|
|
|
for(int m = 0; m < ENS_MAX_MEMBERS; m++)
|
|
|
|
|
{
|
|
|
|
|
bool seen = false;
|
|
|
|
|
for(int r = 0; r < g_ensVoteRows && !seen; r++)
|
|
|
|
|
if(g_ensVoteMember[r * ENS_MAX_MEMBERS + m] != 0.0)
|
|
|
|
|
seen = true;
|
|
|
|
|
if(seen)
|
|
|
|
|
slot[members++] = m;
|
|
|
|
|
}
|
|
|
|
|
if(members < 2)
|
|
|
|
|
return; // nothing to combine
|
|
|
|
|
//--- Declustered, for the reason set out in ReportRegimeStability(): overlapping labels are not
|
|
|
|
|
//--- independent draws, and a mixture fitted on all of them is fitted to ~L copies of each bar.
|
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
|
|
|
int gap = (int)MathMax(m_data.LabelResolutionBars(), 1);
|
feat(baselines): combining-weight fit, MLP cross-validation, all-lags correlation
Three ALGLIB additions, all measurement-only and all under the existing
Run_Alglib_Baselines switch.
MinBLEIC COMBINING WEIGHTS. The live ensemble weights each member by its
own pooled holdout win rate - a defensible prior, but not a fit, and
nothing has ever asked what mixture minimises error on the bars the
members disagreed about. Two individually-mediocre members wrong in
different places can beat one individually better, and a per-member win
rate cannot express that because it never looks at them jointly.
Solved on the simplex (w >= 0, sum w = 1), which is exactly what
MinBLEIC is for. Non-negative because a negative weight asserts "trade
the opposite of this member", a claim ~60 effective observations cannot
support. Least squares on the signed outcome rather than precision:
precision is a STEP function of the threshold that no gradient method
can walk, and optimising a smooth proxy for a step decision is how
c3daded put every operating point 14pp underwater - so the result is
reported in BOTH currencies, the SSE it minimised and the directional
hit rate the mixture would actually have scored against the equal mix.
If the second does not improve, the first is noise.
This needed data that did not exist: g_ensVoteSum accumulates member
contributions and the sum destroys the decomposition, while
g_ensVoteVoterMask records only WHETHER a member voted, never what.
g_ensVoteMember[] keeps them unsummed. The live arithmetic is untouched.
ENS_MAX_MEMBERS is 8 and deliberately larger than MAX_AI_SIGNALS (5):
independent caps, over-allocating is free, and matching them would make
this array silently short the day the registry grows - a cap that has
already dropped a member once without saying so.
MLPKFoldCVLBFGS. Every baseline row carries a binomial SE, which is the
sampling error of SCORING a fixed model and says nothing about how much
the FIT moves. One LBFGS run from one random start can land anywhere,
and a baseline that cleared or missed the bar on luck of initialisation
reads exactly like one that did it on merit. 3 folds, because each is a
full retrain. LBFGS not LM - LM builds a Hessian over ~7,700 weights.
CCorr ALL-LAGS PROFILE. Added BESIDE the MI lag profile, not instead:
MI catches nonlinear dependence and is the stronger negative, which is
why it settled the verdict - but its per-lag permutation null limits it
to ~20 lags. FFT correlation gets every lag in one O(n log n) pass, so
linear structure parked at lag 300 would surface for free. Different
question, not a replacement. Walks CONTIGUOUS bars, unlike everything
else in this file, because a lag index is meaningless otherwise; both
series are mean-centred first since CorrR1D is a raw sum of products;
and the max over columns x lags is judged against a Sidak family of
exactly that size, not a bare 2-sigma line.
fasttransforms.mqh needed its own include - verified that none of
ap/optimization/statistics/solvers/linalg reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:44:36 -04:00
|
|
|
double sMat[], tVec[];
|
|
|
|
|
ArrayResize(sMat, g_ensVoteRows * members);
|
|
|
|
|
ArrayResize(tVec, g_ensVoteRows);
|
|
|
|
|
int rows = 0, lastRow = -1;
|
|
|
|
|
for(int r = 0; r < g_ensVoteRows; r++)
|
|
|
|
|
{
|
|
|
|
|
if(lastRow >= 0 && (r - lastRow) < gap)
|
|
|
|
|
continue;
|
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
|
|
|
bool wl = g_ensVoteLabelBuy[r], ws = g_ensVoteLabelSell[r];
|
feat(baselines): combining-weight fit, MLP cross-validation, all-lags correlation
Three ALGLIB additions, all measurement-only and all under the existing
Run_Alglib_Baselines switch.
MinBLEIC COMBINING WEIGHTS. The live ensemble weights each member by its
own pooled holdout win rate - a defensible prior, but not a fit, and
nothing has ever asked what mixture minimises error on the bars the
members disagreed about. Two individually-mediocre members wrong in
different places can beat one individually better, and a per-member win
rate cannot express that because it never looks at them jointly.
Solved on the simplex (w >= 0, sum w = 1), which is exactly what
MinBLEIC is for. Non-negative because a negative weight asserts "trade
the opposite of this member", a claim ~60 effective observations cannot
support. Least squares on the signed outcome rather than precision:
precision is a STEP function of the threshold that no gradient method
can walk, and optimising a smooth proxy for a step decision is how
c3daded put every operating point 14pp underwater - so the result is
reported in BOTH currencies, the SSE it minimised and the directional
hit rate the mixture would actually have scored against the equal mix.
If the second does not improve, the first is noise.
This needed data that did not exist: g_ensVoteSum accumulates member
contributions and the sum destroys the decomposition, while
g_ensVoteVoterMask records only WHETHER a member voted, never what.
g_ensVoteMember[] keeps them unsummed. The live arithmetic is untouched.
ENS_MAX_MEMBERS is 8 and deliberately larger than MAX_AI_SIGNALS (5):
independent caps, over-allocating is free, and matching them would make
this array silently short the day the registry grows - a cap that has
already dropped a member once without saying so.
MLPKFoldCVLBFGS. Every baseline row carries a binomial SE, which is the
sampling error of SCORING a fixed model and says nothing about how much
the FIT moves. One LBFGS run from one random start can land anywhere,
and a baseline that cleared or missed the bar on luck of initialisation
reads exactly like one that did it on merit. 3 folds, because each is a
full retrain. LBFGS not LM - LM builds a Hessian over ~7,700 weights.
CCorr ALL-LAGS PROFILE. Added BESIDE the MI lag profile, not instead:
MI catches nonlinear dependence and is the stronger negative, which is
why it settled the verdict - but its per-lag permutation null limits it
to ~20 lags. FFT correlation gets every lag in one O(n log n) pass, so
linear structure parked at lag 300 would surface for free. Different
question, not a replacement. Walks CONTIGUOUS bars, unlike everything
else in this file, because a lag index is meaningless otherwise; both
series are mean-centred first since CorrR1D is a raw sum of products;
and the max over columns x lags is judged against a Sidak family of
exactly that size, not a bare 2-sigma line.
fasttransforms.mqh needed its own include - verified that none of
ap/optimization/statistics/solvers/linalg reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:44:36 -04:00
|
|
|
double t = (wl && !ws) ? 1.0 : ((ws && !wl) ? -1.0 : 0.0);
|
|
|
|
|
for(int m = 0; m < members; m++)
|
|
|
|
|
sMat[rows * members + m] = g_ensVoteMember[r * ENS_MAX_MEMBERS + slot[m]] / 100.0;
|
|
|
|
|
tVec[rows++] = t;
|
|
|
|
|
lastRow = r;
|
|
|
|
|
}
|
|
|
|
|
if(rows < BASELINE_MIN_ROWS)
|
|
|
|
|
{
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + StringFormat(": combining weights - %d vote rows decluster to %d independent bars at"
|
feat(baselines): combining-weight fit, MLP cross-validation, all-lags correlation
Three ALGLIB additions, all measurement-only and all under the existing
Run_Alglib_Baselines switch.
MinBLEIC COMBINING WEIGHTS. The live ensemble weights each member by its
own pooled holdout win rate - a defensible prior, but not a fit, and
nothing has ever asked what mixture minimises error on the bars the
members disagreed about. Two individually-mediocre members wrong in
different places can beat one individually better, and a per-member win
rate cannot express that because it never looks at them jointly.
Solved on the simplex (w >= 0, sum w = 1), which is exactly what
MinBLEIC is for. Non-negative because a negative weight asserts "trade
the opposite of this member", a claim ~60 effective observations cannot
support. Least squares on the signed outcome rather than precision:
precision is a STEP function of the threshold that no gradient method
can walk, and optimising a smooth proxy for a step decision is how
c3daded put every operating point 14pp underwater - so the result is
reported in BOTH currencies, the SSE it minimised and the directional
hit rate the mixture would actually have scored against the equal mix.
If the second does not improve, the first is noise.
This needed data that did not exist: g_ensVoteSum accumulates member
contributions and the sum destroys the decomposition, while
g_ensVoteVoterMask records only WHETHER a member voted, never what.
g_ensVoteMember[] keeps them unsummed. The live arithmetic is untouched.
ENS_MAX_MEMBERS is 8 and deliberately larger than MAX_AI_SIGNALS (5):
independent caps, over-allocating is free, and matching them would make
this array silently short the day the registry grows - a cap that has
already dropped a member once without saying so.
MLPKFoldCVLBFGS. Every baseline row carries a binomial SE, which is the
sampling error of SCORING a fixed model and says nothing about how much
the FIT moves. One LBFGS run from one random start can land anywhere,
and a baseline that cleared or missed the bar on luck of initialisation
reads exactly like one that did it on merit. 3 folds, because each is a
full retrain. LBFGS not LM - LM builds a Hessian over ~7,700 weights.
CCorr ALL-LAGS PROFILE. Added BESIDE the MI lag profile, not instead:
MI catches nonlinear dependence and is the stronger negative, which is
why it settled the verdict - but its per-lag permutation null limits it
to ~20 lags. FFT correlation gets every lag in one O(n log n) pass, so
linear structure parked at lag 300 would surface for free. Different
question, not a replacement. Walks CONTIGUOUS bars, unlike everything
else in this file, because a lag index is meaningless otherwise; both
series are mean-centred first since CorrR1D is a raw sum of products;
and the max over columns x lags is judged against a Sidak family of
exactly that size, not a bare 2-sigma line.
fasttransforms.mqh needed its own include - verified that none of
ap/optimization/statistics/solvers/linalg reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:44:36 -04:00
|
|
|
" a %d-bar horizon, too few to fit %d weights on.",
|
|
|
|
|
g_ensVoteRows, rows, gap, members));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
//--- Start at the equal mix, which is also the honest null: "no member deserves more than another".
|
|
|
|
|
double w[], bndl[], bndu[];
|
|
|
|
|
ArrayResize(w, members);
|
|
|
|
|
ArrayResize(bndl, members);
|
|
|
|
|
ArrayResize(bndu, members);
|
|
|
|
|
for(int m = 0; m < members; m++)
|
|
|
|
|
{
|
|
|
|
|
w[m] = 1.0 / members;
|
|
|
|
|
bndl[m] = 0.0;
|
|
|
|
|
bndu[m] = 1.0;
|
|
|
|
|
}
|
|
|
|
|
double sseEqual = CombinerSSE(w, members, sMat, tVec, rows);
|
|
|
|
|
//--- sum(w) == 1, as one linear equality row: [1 1 ... 1 | 1] with ct = 0 meaning "=".
|
|
|
|
|
CMatrixDouble lc(1, members + 1);
|
|
|
|
|
int ct[];
|
|
|
|
|
ArrayResize(ct, 1);
|
|
|
|
|
ct[0] = 0;
|
|
|
|
|
for(int m = 0; m < members; m++)
|
|
|
|
|
lc.Set(0, m, 1.0);
|
|
|
|
|
lc.Set(0, members, 1.0);
|
|
|
|
|
CMinBLEICState state;
|
|
|
|
|
CMinBLEICReport rep;
|
|
|
|
|
//--- CreateF, i.e. NUMERICAL gradients: with at most ENS_MAX_MEMBERS unknowns a finite-difference
|
|
|
|
|
//--- gradient is members+1 evaluations, which is nothing against the alternative of hand-deriving
|
|
|
|
|
//--- and maintaining an analytic one for a diagnostic.
|
|
|
|
|
CMinBLEIC::MinBLEICCreateF(members, w, 1.0e-6, state);
|
|
|
|
|
CMinBLEIC::MinBLEICSetBC(state, bndl, bndu);
|
|
|
|
|
CMinBLEIC::MinBLEICSetLC(state, lc, ct, 1);
|
|
|
|
|
CMinBLEIC::MinBLEICSetCond(state, 0.0, 0.0, 1.0e-8, 200);
|
|
|
|
|
//--- Driven by hand rather than through MinBLEICOptimize's delegate: the objective needs the two
|
|
|
|
|
//--- local matrices above, and a CNDimensional_Func subclass would have to smuggle them through a
|
|
|
|
|
//--- CObject to reach them.
|
|
|
|
|
double trial[];
|
|
|
|
|
ArrayResize(trial, members);
|
|
|
|
|
while(CMinBLEIC::MinBLEICIteration(state))
|
|
|
|
|
{
|
|
|
|
|
if(!state.m_needf)
|
|
|
|
|
continue;
|
|
|
|
|
for(int m = 0; m < members; m++)
|
|
|
|
|
trial[m] = state.m_x[m];
|
|
|
|
|
state.m_f = CombinerSSE(trial, members, sMat, tVec, rows);
|
|
|
|
|
}
|
|
|
|
|
double fitted[];
|
|
|
|
|
CMinBLEIC::MinBLEICResults(state, fitted, rep);
|
|
|
|
|
if(rep.m_terminationtype <= 0 || ArraySize(fitted) < members)
|
|
|
|
|
{
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + StringFormat(": combining weights - MinBLEIC did not converge (termination %d).",
|
feat(baselines): combining-weight fit, MLP cross-validation, all-lags correlation
Three ALGLIB additions, all measurement-only and all under the existing
Run_Alglib_Baselines switch.
MinBLEIC COMBINING WEIGHTS. The live ensemble weights each member by its
own pooled holdout win rate - a defensible prior, but not a fit, and
nothing has ever asked what mixture minimises error on the bars the
members disagreed about. Two individually-mediocre members wrong in
different places can beat one individually better, and a per-member win
rate cannot express that because it never looks at them jointly.
Solved on the simplex (w >= 0, sum w = 1), which is exactly what
MinBLEIC is for. Non-negative because a negative weight asserts "trade
the opposite of this member", a claim ~60 effective observations cannot
support. Least squares on the signed outcome rather than precision:
precision is a STEP function of the threshold that no gradient method
can walk, and optimising a smooth proxy for a step decision is how
c3daded put every operating point 14pp underwater - so the result is
reported in BOTH currencies, the SSE it minimised and the directional
hit rate the mixture would actually have scored against the equal mix.
If the second does not improve, the first is noise.
This needed data that did not exist: g_ensVoteSum accumulates member
contributions and the sum destroys the decomposition, while
g_ensVoteVoterMask records only WHETHER a member voted, never what.
g_ensVoteMember[] keeps them unsummed. The live arithmetic is untouched.
ENS_MAX_MEMBERS is 8 and deliberately larger than MAX_AI_SIGNALS (5):
independent caps, over-allocating is free, and matching them would make
this array silently short the day the registry grows - a cap that has
already dropped a member once without saying so.
MLPKFoldCVLBFGS. Every baseline row carries a binomial SE, which is the
sampling error of SCORING a fixed model and says nothing about how much
the FIT moves. One LBFGS run from one random start can land anywhere,
and a baseline that cleared or missed the bar on luck of initialisation
reads exactly like one that did it on merit. 3 folds, because each is a
full retrain. LBFGS not LM - LM builds a Hessian over ~7,700 weights.
CCorr ALL-LAGS PROFILE. Added BESIDE the MI lag profile, not instead:
MI catches nonlinear dependence and is the stronger negative, which is
why it settled the verdict - but its per-lag permutation null limits it
to ~20 lags. FFT correlation gets every lag in one O(n log n) pass, so
linear structure parked at lag 300 would surface for free. Different
question, not a replacement. Walks CONTIGUOUS bars, unlike everything
else in this file, because a lag index is meaningless otherwise; both
series are mean-centred first since CorrR1D is a raw sum of products;
and the max over columns x lags is judged against a Sidak family of
exactly that size, not a bare 2-sigma line.
fasttransforms.mqh needed its own include - verified that none of
ap/optimization/statistics/solvers/linalg reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:44:36 -04:00
|
|
|
rep.m_terminationtype));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
double sseFit = CombinerSSE(fitted, members, sMat, tVec, rows);
|
|
|
|
|
string line = "";
|
|
|
|
|
for(int m = 0; m < members; m++)
|
|
|
|
|
//--- Slot index, not a name: g_aiSignals[] is declared in Warrior_EA.mq5, which is compiled
|
|
|
|
|
//--- AFTER this partial, so the registry is not reachable from here. The startup census line
|
|
|
|
|
//--- maps index to model id.
|
|
|
|
|
line += StringFormat(" m%d=%.3f", slot[m], fitted[m]);
|
|
|
|
|
//--- ...and the same mixture judged on the decision the EA actually makes: sign of the blend
|
|
|
|
|
//--- against which direction paid. This is the number that decides whether the fit meant anything.
|
|
|
|
|
int fitCalls = 0, fitHits = 0, eqCalls = 0, eqHits = 0;
|
|
|
|
|
for(int r = 0; r < rows; r++)
|
|
|
|
|
{
|
|
|
|
|
double bf = 0.0, be = 0.0;
|
|
|
|
|
for(int m = 0; m < members; m++)
|
|
|
|
|
{
|
|
|
|
|
bf += fitted[m] * sMat[r * members + m];
|
|
|
|
|
be += sMat[r * members + m] / members;
|
|
|
|
|
}
|
|
|
|
|
if(bf != 0.0 && tVec[r] != 0.0)
|
|
|
|
|
{
|
|
|
|
|
fitCalls++;
|
|
|
|
|
if((bf > 0.0) == (tVec[r] > 0.0))
|
|
|
|
|
fitHits++;
|
|
|
|
|
}
|
|
|
|
|
if(be != 0.0 && tVec[r] != 0.0)
|
|
|
|
|
{
|
|
|
|
|
eqCalls++;
|
|
|
|
|
if((be > 0.0) == (tVec[r] > 0.0))
|
|
|
|
|
eqHits++;
|
|
|
|
|
}
|
|
|
|
|
}
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + StringFormat(": combining weights over %d independent bars -%s | SSE %.4f fitted vs"
|
feat(baselines): combining-weight fit, MLP cross-validation, all-lags correlation
Three ALGLIB additions, all measurement-only and all under the existing
Run_Alglib_Baselines switch.
MinBLEIC COMBINING WEIGHTS. The live ensemble weights each member by its
own pooled holdout win rate - a defensible prior, but not a fit, and
nothing has ever asked what mixture minimises error on the bars the
members disagreed about. Two individually-mediocre members wrong in
different places can beat one individually better, and a per-member win
rate cannot express that because it never looks at them jointly.
Solved on the simplex (w >= 0, sum w = 1), which is exactly what
MinBLEIC is for. Non-negative because a negative weight asserts "trade
the opposite of this member", a claim ~60 effective observations cannot
support. Least squares on the signed outcome rather than precision:
precision is a STEP function of the threshold that no gradient method
can walk, and optimising a smooth proxy for a step decision is how
c3daded put every operating point 14pp underwater - so the result is
reported in BOTH currencies, the SSE it minimised and the directional
hit rate the mixture would actually have scored against the equal mix.
If the second does not improve, the first is noise.
This needed data that did not exist: g_ensVoteSum accumulates member
contributions and the sum destroys the decomposition, while
g_ensVoteVoterMask records only WHETHER a member voted, never what.
g_ensVoteMember[] keeps them unsummed. The live arithmetic is untouched.
ENS_MAX_MEMBERS is 8 and deliberately larger than MAX_AI_SIGNALS (5):
independent caps, over-allocating is free, and matching them would make
this array silently short the day the registry grows - a cap that has
already dropped a member once without saying so.
MLPKFoldCVLBFGS. Every baseline row carries a binomial SE, which is the
sampling error of SCORING a fixed model and says nothing about how much
the FIT moves. One LBFGS run from one random start can land anywhere,
and a baseline that cleared or missed the bar on luck of initialisation
reads exactly like one that did it on merit. 3 folds, because each is a
full retrain. LBFGS not LM - LM builds a Hessian over ~7,700 weights.
CCorr ALL-LAGS PROFILE. Added BESIDE the MI lag profile, not instead:
MI catches nonlinear dependence and is the stronger negative, which is
why it settled the verdict - but its per-lag permutation null limits it
to ~20 lags. FFT correlation gets every lag in one O(n log n) pass, so
linear structure parked at lag 300 would surface for free. Different
question, not a replacement. Walks CONTIGUOUS bars, unlike everything
else in this file, because a lag index is meaningless otherwise; both
series are mean-centred first since CorrR1D is a raw sum of products;
and the max over columns x lags is judged against a Sidak family of
exactly that size, not a bare 2-sigma line.
fasttransforms.mqh needed its own include - verified that none of
ap/optimization/statistics/solvers/linalg reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:44:36 -04:00
|
|
|
" %.4f at the equal mix | directional hit rate %.1f%% (%d) vs %.1f%% (%d)"
|
|
|
|
|
" equal-weighted. %s",
|
|
|
|
|
rows, line, sseFit, sseEqual,
|
|
|
|
|
(fitCalls > 0 ? 100.0 * fitHits / fitCalls : 0.0), fitCalls,
|
|
|
|
|
(eqCalls > 0 ? 100.0 * eqHits / eqCalls : 0.0), eqCalls,
|
|
|
|
|
"Reported only - the live weights are unchanged, because moving them moves"
|
|
|
|
|
" the vote the deploy gate certifies."));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Mean squared error of a weighted blend against the signed outcome.|
|
|
|
|
|
//| The objective MinBLEIC minimises above, and the number the equal |
|
|
|
|
|
//| mix is scored on, so both readings come from one expression. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
double CBaselineComparator::CombinerSSE(const double &w[], const int members, const double &s[],
|
feat(baselines): combining-weight fit, MLP cross-validation, all-lags correlation
Three ALGLIB additions, all measurement-only and all under the existing
Run_Alglib_Baselines switch.
MinBLEIC COMBINING WEIGHTS. The live ensemble weights each member by its
own pooled holdout win rate - a defensible prior, but not a fit, and
nothing has ever asked what mixture minimises error on the bars the
members disagreed about. Two individually-mediocre members wrong in
different places can beat one individually better, and a per-member win
rate cannot express that because it never looks at them jointly.
Solved on the simplex (w >= 0, sum w = 1), which is exactly what
MinBLEIC is for. Non-negative because a negative weight asserts "trade
the opposite of this member", a claim ~60 effective observations cannot
support. Least squares on the signed outcome rather than precision:
precision is a STEP function of the threshold that no gradient method
can walk, and optimising a smooth proxy for a step decision is how
c3daded put every operating point 14pp underwater - so the result is
reported in BOTH currencies, the SSE it minimised and the directional
hit rate the mixture would actually have scored against the equal mix.
If the second does not improve, the first is noise.
This needed data that did not exist: g_ensVoteSum accumulates member
contributions and the sum destroys the decomposition, while
g_ensVoteVoterMask records only WHETHER a member voted, never what.
g_ensVoteMember[] keeps them unsummed. The live arithmetic is untouched.
ENS_MAX_MEMBERS is 8 and deliberately larger than MAX_AI_SIGNALS (5):
independent caps, over-allocating is free, and matching them would make
this array silently short the day the registry grows - a cap that has
already dropped a member once without saying so.
MLPKFoldCVLBFGS. Every baseline row carries a binomial SE, which is the
sampling error of SCORING a fixed model and says nothing about how much
the FIT moves. One LBFGS run from one random start can land anywhere,
and a baseline that cleared or missed the bar on luck of initialisation
reads exactly like one that did it on merit. 3 folds, because each is a
full retrain. LBFGS not LM - LM builds a Hessian over ~7,700 weights.
CCorr ALL-LAGS PROFILE. Added BESIDE the MI lag profile, not instead:
MI catches nonlinear dependence and is the stronger negative, which is
why it settled the verdict - but its per-lag permutation null limits it
to ~20 lags. FFT correlation gets every lag in one O(n log n) pass, so
linear structure parked at lag 300 would surface for free. Different
question, not a replacement. Walks CONTIGUOUS bars, unlike everything
else in this file, because a lag index is meaningless otherwise; both
series are mean-centred first since CorrR1D is a raw sum of products;
and the max over columns x lags is judged against a Sidak family of
exactly that size, not a bare 2-sigma line.
fasttransforms.mqh needed its own include - verified that none of
ap/optimization/statistics/solvers/linalg reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:44:36 -04:00
|
|
|
const double &t[], const int rows)
|
|
|
|
|
{
|
|
|
|
|
if(rows <= 0 || members <= 0)
|
|
|
|
|
return 0.0;
|
|
|
|
|
double acc = 0.0;
|
|
|
|
|
for(int r = 0; r < rows; r++)
|
|
|
|
|
{
|
|
|
|
|
double blend = 0.0;
|
|
|
|
|
for(int m = 0; m < members; m++)
|
|
|
|
|
blend += w[m] * s[r * members + m];
|
|
|
|
|
double e = blend - t[r];
|
|
|
|
|
acc += e * e;
|
|
|
|
|
}
|
|
|
|
|
return acc / rows;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
2026-08-22 00:30:14 -04:00
|
|
|
//| EVERY LAG AT ONCE, via FFT cross-correlation. |
|
feat(baselines): combining-weight fit, MLP cross-validation, all-lags correlation
Three ALGLIB additions, all measurement-only and all under the existing
Run_Alglib_Baselines switch.
MinBLEIC COMBINING WEIGHTS. The live ensemble weights each member by its
own pooled holdout win rate - a defensible prior, but not a fit, and
nothing has ever asked what mixture minimises error on the bars the
members disagreed about. Two individually-mediocre members wrong in
different places can beat one individually better, and a per-member win
rate cannot express that because it never looks at them jointly.
Solved on the simplex (w >= 0, sum w = 1), which is exactly what
MinBLEIC is for. Non-negative because a negative weight asserts "trade
the opposite of this member", a claim ~60 effective observations cannot
support. Least squares on the signed outcome rather than precision:
precision is a STEP function of the threshold that no gradient method
can walk, and optimising a smooth proxy for a step decision is how
c3daded put every operating point 14pp underwater - so the result is
reported in BOTH currencies, the SSE it minimised and the directional
hit rate the mixture would actually have scored against the equal mix.
If the second does not improve, the first is noise.
This needed data that did not exist: g_ensVoteSum accumulates member
contributions and the sum destroys the decomposition, while
g_ensVoteVoterMask records only WHETHER a member voted, never what.
g_ensVoteMember[] keeps them unsummed. The live arithmetic is untouched.
ENS_MAX_MEMBERS is 8 and deliberately larger than MAX_AI_SIGNALS (5):
independent caps, over-allocating is free, and matching them would make
this array silently short the day the registry grows - a cap that has
already dropped a member once without saying so.
MLPKFoldCVLBFGS. Every baseline row carries a binomial SE, which is the
sampling error of SCORING a fixed model and says nothing about how much
the FIT moves. One LBFGS run from one random start can land anywhere,
and a baseline that cleared or missed the bar on luck of initialisation
reads exactly like one that did it on merit. 3 folds, because each is a
full retrain. LBFGS not LM - LM builds a Hessian over ~7,700 weights.
CCorr ALL-LAGS PROFILE. Added BESIDE the MI lag profile, not instead:
MI catches nonlinear dependence and is the stronger negative, which is
why it settled the verdict - but its per-lag permutation null limits it
to ~20 lags. FFT correlation gets every lag in one O(n log n) pass, so
linear structure parked at lag 300 would surface for free. Different
question, not a replacement. Walks CONTIGUOUS bars, unlike everything
else in this file, because a lag index is meaningless otherwise; both
series are mean-centred first since CorrR1D is a raw sum of products;
and the max over columns x lags is judged against a Sidak family of
exactly that size, not a bare 2-sigma line.
fasttransforms.mqh needed its own include - verified that none of
ap/optimization/statistics/solvers/linalg reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:44:36 -04:00
|
|
|
//+------------------------------------------------------------------+
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
void CBaselineComparator::ReportLinearLagProfile(const int bars, const int oosCutoff)
|
feat(baselines): combining-weight fit, MLP cross-validation, all-lags correlation
Three ALGLIB additions, all measurement-only and all under the existing
Run_Alglib_Baselines switch.
MinBLEIC COMBINING WEIGHTS. The live ensemble weights each member by its
own pooled holdout win rate - a defensible prior, but not a fit, and
nothing has ever asked what mixture minimises error on the bars the
members disagreed about. Two individually-mediocre members wrong in
different places can beat one individually better, and a per-member win
rate cannot express that because it never looks at them jointly.
Solved on the simplex (w >= 0, sum w = 1), which is exactly what
MinBLEIC is for. Non-negative because a negative weight asserts "trade
the opposite of this member", a claim ~60 effective observations cannot
support. Least squares on the signed outcome rather than precision:
precision is a STEP function of the threshold that no gradient method
can walk, and optimising a smooth proxy for a step decision is how
c3daded put every operating point 14pp underwater - so the result is
reported in BOTH currencies, the SSE it minimised and the directional
hit rate the mixture would actually have scored against the equal mix.
If the second does not improve, the first is noise.
This needed data that did not exist: g_ensVoteSum accumulates member
contributions and the sum destroys the decomposition, while
g_ensVoteVoterMask records only WHETHER a member voted, never what.
g_ensVoteMember[] keeps them unsummed. The live arithmetic is untouched.
ENS_MAX_MEMBERS is 8 and deliberately larger than MAX_AI_SIGNALS (5):
independent caps, over-allocating is free, and matching them would make
this array silently short the day the registry grows - a cap that has
already dropped a member once without saying so.
MLPKFoldCVLBFGS. Every baseline row carries a binomial SE, which is the
sampling error of SCORING a fixed model and says nothing about how much
the FIT moves. One LBFGS run from one random start can land anywhere,
and a baseline that cleared or missed the bar on luck of initialisation
reads exactly like one that did it on merit. 3 folds, because each is a
full retrain. LBFGS not LM - LM builds a Hessian over ~7,700 weights.
CCorr ALL-LAGS PROFILE. Added BESIDE the MI lag profile, not instead:
MI catches nonlinear dependence and is the stronger negative, which is
why it settled the verdict - but its per-lag permutation null limits it
to ~20 lags. FFT correlation gets every lag in one O(n log n) pass, so
linear structure parked at lag 300 would surface for free. Different
question, not a replacement. Walks CONTIGUOUS bars, unlike everything
else in this file, because a lag index is meaningless otherwise; both
series are mean-centred first since CorrR1D is a raw sum of products;
and the max over columns x lags is judged against a Sidak family of
exactly that size, not a bare 2-sigma line.
fasttransforms.mqh needed its own include - verified that none of
ap/optimization/statistics/solvers/linalg reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:44:36 -04:00
|
|
|
{
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
int nvars = m_data.FeaturesPerBar();
|
|
|
|
|
int width = m_data.HistoryBars() * m_data.FeaturesPerBar();
|
feat(baselines): combining-weight fit, MLP cross-validation, all-lags correlation
Three ALGLIB additions, all measurement-only and all under the existing
Run_Alglib_Baselines switch.
MinBLEIC COMBINING WEIGHTS. The live ensemble weights each member by its
own pooled holdout win rate - a defensible prior, but not a fit, and
nothing has ever asked what mixture minimises error on the bars the
members disagreed about. Two individually-mediocre members wrong in
different places can beat one individually better, and a per-member win
rate cannot express that because it never looks at them jointly.
Solved on the simplex (w >= 0, sum w = 1), which is exactly what
MinBLEIC is for. Non-negative because a negative weight asserts "trade
the opposite of this member", a claim ~60 effective observations cannot
support. Least squares on the signed outcome rather than precision:
precision is a STEP function of the threshold that no gradient method
can walk, and optimising a smooth proxy for a step decision is how
c3daded put every operating point 14pp underwater - so the result is
reported in BOTH currencies, the SSE it minimised and the directional
hit rate the mixture would actually have scored against the equal mix.
If the second does not improve, the first is noise.
This needed data that did not exist: g_ensVoteSum accumulates member
contributions and the sum destroys the decomposition, while
g_ensVoteVoterMask records only WHETHER a member voted, never what.
g_ensVoteMember[] keeps them unsummed. The live arithmetic is untouched.
ENS_MAX_MEMBERS is 8 and deliberately larger than MAX_AI_SIGNALS (5):
independent caps, over-allocating is free, and matching them would make
this array silently short the day the registry grows - a cap that has
already dropped a member once without saying so.
MLPKFoldCVLBFGS. Every baseline row carries a binomial SE, which is the
sampling error of SCORING a fixed model and says nothing about how much
the FIT moves. One LBFGS run from one random start can land anywhere,
and a baseline that cleared or missed the bar on luck of initialisation
reads exactly like one that did it on merit. 3 folds, because each is a
full retrain. LBFGS not LM - LM builds a Hessian over ~7,700 weights.
CCorr ALL-LAGS PROFILE. Added BESIDE the MI lag profile, not instead:
MI catches nonlinear dependence and is the stronger negative, which is
why it settled the verdict - but its per-lag permutation null limits it
to ~20 lags. FFT correlation gets every lag in one O(n log n) pass, so
linear structure parked at lag 300 would surface for free. Different
question, not a replacement. Walks CONTIGUOUS bars, unlike everything
else in this file, because a lag index is meaningless otherwise; both
series are mean-centred first since CorrR1D is a raw sum of products;
and the max over columns x lags is judged against a Sidak family of
exactly that size, not a bare 2-sigma line.
fasttransforms.mqh needed its own include - verified that none of
ap/optimization/statistics/solvers/linalg reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:44:36 -04:00
|
|
|
if(nvars < 1 || width <= 0)
|
|
|
|
|
return;
|
|
|
|
|
//--- Newest-first walk over a contiguous OOS stretch, capped so one FFT stays cheap.
|
|
|
|
|
int want = (int)MathMin(BASELINE_MAX_SCORE_ROWS, oosCutoff);
|
|
|
|
|
double x[], series[], target[];
|
|
|
|
|
ArrayResize(x, width);
|
|
|
|
|
ArrayResize(series, want * nvars);
|
|
|
|
|
ArrayResize(target, want);
|
|
|
|
|
int n = 0;
|
|
|
|
|
for(int r = 0; r < oosCutoff && n < want; r++)
|
|
|
|
|
{
|
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
|
|
|
if(r + m_data.HistoryBars() - 1 >= bars)
|
feat(baselines): combining-weight fit, MLP cross-validation, all-lags correlation
Three ALGLIB additions, all measurement-only and all under the existing
Run_Alglib_Baselines switch.
MinBLEIC COMBINING WEIGHTS. The live ensemble weights each member by its
own pooled holdout win rate - a defensible prior, but not a fit, and
nothing has ever asked what mixture minimises error on the bars the
members disagreed about. Two individually-mediocre members wrong in
different places can beat one individually better, and a per-member win
rate cannot express that because it never looks at them jointly.
Solved on the simplex (w >= 0, sum w = 1), which is exactly what
MinBLEIC is for. Non-negative because a negative weight asserts "trade
the opposite of this member", a claim ~60 effective observations cannot
support. Least squares on the signed outcome rather than precision:
precision is a STEP function of the threshold that no gradient method
can walk, and optimising a smooth proxy for a step decision is how
c3daded put every operating point 14pp underwater - so the result is
reported in BOTH currencies, the SSE it minimised and the directional
hit rate the mixture would actually have scored against the equal mix.
If the second does not improve, the first is noise.
This needed data that did not exist: g_ensVoteSum accumulates member
contributions and the sum destroys the decomposition, while
g_ensVoteVoterMask records only WHETHER a member voted, never what.
g_ensVoteMember[] keeps them unsummed. The live arithmetic is untouched.
ENS_MAX_MEMBERS is 8 and deliberately larger than MAX_AI_SIGNALS (5):
independent caps, over-allocating is free, and matching them would make
this array silently short the day the registry grows - a cap that has
already dropped a member once without saying so.
MLPKFoldCVLBFGS. Every baseline row carries a binomial SE, which is the
sampling error of SCORING a fixed model and says nothing about how much
the FIT moves. One LBFGS run from one random start can land anywhere,
and a baseline that cleared or missed the bar on luck of initialisation
reads exactly like one that did it on merit. 3 folds, because each is a
full retrain. LBFGS not LM - LM builds a Hessian over ~7,700 weights.
CCorr ALL-LAGS PROFILE. Added BESIDE the MI lag profile, not instead:
MI catches nonlinear dependence and is the stronger negative, which is
why it settled the verdict - but its per-lag permutation null limits it
to ~20 lags. FFT correlation gets every lag in one O(n log n) pass, so
linear structure parked at lag 300 would surface for free. Different
question, not a replacement. Walks CONTIGUOUS bars, unlike everything
else in this file, because a lag index is meaningless otherwise; both
series are mean-centred first since CorrR1D is a raw sum of products;
and the max over columns x lags is judged against a Sidak family of
exactly that size, not a bare 2-sigma line.
fasttransforms.mqh needed its own include - verified that none of
ap/optimization/statistics/solvers/linalg reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:44:36 -04:00
|
|
|
break;
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
if(!m_data.HasLabel(r))
|
feat(baselines): combining-weight fit, MLP cross-validation, all-lags correlation
Three ALGLIB additions, all measurement-only and all under the existing
Run_Alglib_Baselines switch.
MinBLEIC COMBINING WEIGHTS. The live ensemble weights each member by its
own pooled holdout win rate - a defensible prior, but not a fit, and
nothing has ever asked what mixture minimises error on the bars the
members disagreed about. Two individually-mediocre members wrong in
different places can beat one individually better, and a per-member win
rate cannot express that because it never looks at them jointly.
Solved on the simplex (w >= 0, sum w = 1), which is exactly what
MinBLEIC is for. Non-negative because a negative weight asserts "trade
the opposite of this member", a claim ~60 effective observations cannot
support. Least squares on the signed outcome rather than precision:
precision is a STEP function of the threshold that no gradient method
can walk, and optimising a smooth proxy for a step decision is how
c3daded put every operating point 14pp underwater - so the result is
reported in BOTH currencies, the SSE it minimised and the directional
hit rate the mixture would actually have scored against the equal mix.
If the second does not improve, the first is noise.
This needed data that did not exist: g_ensVoteSum accumulates member
contributions and the sum destroys the decomposition, while
g_ensVoteVoterMask records only WHETHER a member voted, never what.
g_ensVoteMember[] keeps them unsummed. The live arithmetic is untouched.
ENS_MAX_MEMBERS is 8 and deliberately larger than MAX_AI_SIGNALS (5):
independent caps, over-allocating is free, and matching them would make
this array silently short the day the registry grows - a cap that has
already dropped a member once without saying so.
MLPKFoldCVLBFGS. Every baseline row carries a binomial SE, which is the
sampling error of SCORING a fixed model and says nothing about how much
the FIT moves. One LBFGS run from one random start can land anywhere,
and a baseline that cleared or missed the bar on luck of initialisation
reads exactly like one that did it on merit. 3 folds, because each is a
full retrain. LBFGS not LM - LM builds a Hessian over ~7,700 weights.
CCorr ALL-LAGS PROFILE. Added BESIDE the MI lag profile, not instead:
MI catches nonlinear dependence and is the stronger negative, which is
why it settled the verdict - but its per-lag permutation null limits it
to ~20 lags. FFT correlation gets every lag in one O(n log n) pass, so
linear structure parked at lag 300 would surface for free. Different
question, not a replacement. Walks CONTIGUOUS bars, unlike everything
else in this file, because a lag index is meaningless otherwise; both
series are mean-centred first since CorrR1D is a raw sum of products;
and the max over columns x lags is judged against a Sidak family of
exactly that size, not a bare 2-sigma line.
fasttransforms.mqh needed its own include - verified that none of
ap/optimization/statistics/solvers/linalg reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:44:36 -04:00
|
|
|
break; // a hole breaks contiguity - stop rather than splice across it
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
if(!m_data.RowFeatures(r, width, x))
|
feat(baselines): combining-weight fit, MLP cross-validation, all-lags correlation
Three ALGLIB additions, all measurement-only and all under the existing
Run_Alglib_Baselines switch.
MinBLEIC COMBINING WEIGHTS. The live ensemble weights each member by its
own pooled holdout win rate - a defensible prior, but not a fit, and
nothing has ever asked what mixture minimises error on the bars the
members disagreed about. Two individually-mediocre members wrong in
different places can beat one individually better, and a per-member win
rate cannot express that because it never looks at them jointly.
Solved on the simplex (w >= 0, sum w = 1), which is exactly what
MinBLEIC is for. Non-negative because a negative weight asserts "trade
the opposite of this member", a claim ~60 effective observations cannot
support. Least squares on the signed outcome rather than precision:
precision is a STEP function of the threshold that no gradient method
can walk, and optimising a smooth proxy for a step decision is how
c3daded put every operating point 14pp underwater - so the result is
reported in BOTH currencies, the SSE it minimised and the directional
hit rate the mixture would actually have scored against the equal mix.
If the second does not improve, the first is noise.
This needed data that did not exist: g_ensVoteSum accumulates member
contributions and the sum destroys the decomposition, while
g_ensVoteVoterMask records only WHETHER a member voted, never what.
g_ensVoteMember[] keeps them unsummed. The live arithmetic is untouched.
ENS_MAX_MEMBERS is 8 and deliberately larger than MAX_AI_SIGNALS (5):
independent caps, over-allocating is free, and matching them would make
this array silently short the day the registry grows - a cap that has
already dropped a member once without saying so.
MLPKFoldCVLBFGS. Every baseline row carries a binomial SE, which is the
sampling error of SCORING a fixed model and says nothing about how much
the FIT moves. One LBFGS run from one random start can land anywhere,
and a baseline that cleared or missed the bar on luck of initialisation
reads exactly like one that did it on merit. 3 folds, because each is a
full retrain. LBFGS not LM - LM builds a Hessian over ~7,700 weights.
CCorr ALL-LAGS PROFILE. Added BESIDE the MI lag profile, not instead:
MI catches nonlinear dependence and is the stronger negative, which is
why it settled the verdict - but its per-lag permutation null limits it
to ~20 lags. FFT correlation gets every lag in one O(n log n) pass, so
linear structure parked at lag 300 would surface for free. Different
question, not a replacement. Walks CONTIGUOUS bars, unlike everything
else in this file, because a lag index is meaningless otherwise; both
series are mean-centred first since CorrR1D is a raw sum of products;
and the max over columns x lags is judged against a Sidak family of
exactly that size, not a bare 2-sigma line.
fasttransforms.mqh needed its own include - verified that none of
ap/optimization/statistics/solvers/linalg reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:44:36 -04:00
|
|
|
break;
|
|
|
|
|
for(int f = 0; f < nvars; f++)
|
|
|
|
|
series[n * nvars + f] = x[width - nvars + f];
|
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
|
|
|
//--- Signed label. Neutral bars carry no direction and enter as 0 rather than being dropped,
|
|
|
|
|
//--- which would break the contiguity a lag index depends on.
|
|
|
|
|
bool wl = m_data.IsBuyLabel(r), ws = m_data.IsSellLabel(r);
|
feat(baselines): combining-weight fit, MLP cross-validation, all-lags correlation
Three ALGLIB additions, all measurement-only and all under the existing
Run_Alglib_Baselines switch.
MinBLEIC COMBINING WEIGHTS. The live ensemble weights each member by its
own pooled holdout win rate - a defensible prior, but not a fit, and
nothing has ever asked what mixture minimises error on the bars the
members disagreed about. Two individually-mediocre members wrong in
different places can beat one individually better, and a per-member win
rate cannot express that because it never looks at them jointly.
Solved on the simplex (w >= 0, sum w = 1), which is exactly what
MinBLEIC is for. Non-negative because a negative weight asserts "trade
the opposite of this member", a claim ~60 effective observations cannot
support. Least squares on the signed outcome rather than precision:
precision is a STEP function of the threshold that no gradient method
can walk, and optimising a smooth proxy for a step decision is how
c3daded put every operating point 14pp underwater - so the result is
reported in BOTH currencies, the SSE it minimised and the directional
hit rate the mixture would actually have scored against the equal mix.
If the second does not improve, the first is noise.
This needed data that did not exist: g_ensVoteSum accumulates member
contributions and the sum destroys the decomposition, while
g_ensVoteVoterMask records only WHETHER a member voted, never what.
g_ensVoteMember[] keeps them unsummed. The live arithmetic is untouched.
ENS_MAX_MEMBERS is 8 and deliberately larger than MAX_AI_SIGNALS (5):
independent caps, over-allocating is free, and matching them would make
this array silently short the day the registry grows - a cap that has
already dropped a member once without saying so.
MLPKFoldCVLBFGS. Every baseline row carries a binomial SE, which is the
sampling error of SCORING a fixed model and says nothing about how much
the FIT moves. One LBFGS run from one random start can land anywhere,
and a baseline that cleared or missed the bar on luck of initialisation
reads exactly like one that did it on merit. 3 folds, because each is a
full retrain. LBFGS not LM - LM builds a Hessian over ~7,700 weights.
CCorr ALL-LAGS PROFILE. Added BESIDE the MI lag profile, not instead:
MI catches nonlinear dependence and is the stronger negative, which is
why it settled the verdict - but its per-lag permutation null limits it
to ~20 lags. FFT correlation gets every lag in one O(n log n) pass, so
linear structure parked at lag 300 would surface for free. Different
question, not a replacement. Walks CONTIGUOUS bars, unlike everything
else in this file, because a lag index is meaningless otherwise; both
series are mean-centred first since CorrR1D is a raw sum of products;
and the max over columns x lags is judged against a Sidak family of
exactly that size, not a bare 2-sigma line.
fasttransforms.mqh needed its own include - verified that none of
ap/optimization/statistics/solvers/linalg reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:44:36 -04:00
|
|
|
target[n] = (wl && !ws) ? 1.0 : ((ws && !wl) ? -1.0 : 0.0);
|
|
|
|
|
n++;
|
|
|
|
|
}
|
|
|
|
|
if(n < BASELINE_MIN_ROWS)
|
|
|
|
|
{
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + StringFormat(": linear lag profile skipped - only %d contiguous OOS bars.", n));
|
feat(baselines): combining-weight fit, MLP cross-validation, all-lags correlation
Three ALGLIB additions, all measurement-only and all under the existing
Run_Alglib_Baselines switch.
MinBLEIC COMBINING WEIGHTS. The live ensemble weights each member by its
own pooled holdout win rate - a defensible prior, but not a fit, and
nothing has ever asked what mixture minimises error on the bars the
members disagreed about. Two individually-mediocre members wrong in
different places can beat one individually better, and a per-member win
rate cannot express that because it never looks at them jointly.
Solved on the simplex (w >= 0, sum w = 1), which is exactly what
MinBLEIC is for. Non-negative because a negative weight asserts "trade
the opposite of this member", a claim ~60 effective observations cannot
support. Least squares on the signed outcome rather than precision:
precision is a STEP function of the threshold that no gradient method
can walk, and optimising a smooth proxy for a step decision is how
c3daded put every operating point 14pp underwater - so the result is
reported in BOTH currencies, the SSE it minimised and the directional
hit rate the mixture would actually have scored against the equal mix.
If the second does not improve, the first is noise.
This needed data that did not exist: g_ensVoteSum accumulates member
contributions and the sum destroys the decomposition, while
g_ensVoteVoterMask records only WHETHER a member voted, never what.
g_ensVoteMember[] keeps them unsummed. The live arithmetic is untouched.
ENS_MAX_MEMBERS is 8 and deliberately larger than MAX_AI_SIGNALS (5):
independent caps, over-allocating is free, and matching them would make
this array silently short the day the registry grows - a cap that has
already dropped a member once without saying so.
MLPKFoldCVLBFGS. Every baseline row carries a binomial SE, which is the
sampling error of SCORING a fixed model and says nothing about how much
the FIT moves. One LBFGS run from one random start can land anywhere,
and a baseline that cleared or missed the bar on luck of initialisation
reads exactly like one that did it on merit. 3 folds, because each is a
full retrain. LBFGS not LM - LM builds a Hessian over ~7,700 weights.
CCorr ALL-LAGS PROFILE. Added BESIDE the MI lag profile, not instead:
MI catches nonlinear dependence and is the stronger negative, which is
why it settled the verdict - but its per-lag permutation null limits it
to ~20 lags. FFT correlation gets every lag in one O(n log n) pass, so
linear structure parked at lag 300 would surface for free. Different
question, not a replacement. Walks CONTIGUOUS bars, unlike everything
else in this file, because a lag index is meaningless otherwise; both
series are mean-centred first since CorrR1D is a raw sum of products;
and the max over columns x lags is judged against a Sidak family of
exactly that size, not a bare 2-sigma line.
fasttransforms.mqh needed its own include - verified that none of
ap/optimization/statistics/solvers/linalg reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:44:36 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
//--- The band a correlation of exactly nothing still fluctuates inside, ~1/sqrt(n) per lag. Read
|
|
|
|
|
//--- against the MAXIMUM over all lags and every column, so it is a family of n*nvars draws - the
|
|
|
|
|
//--- best-of-N null, not a single one. Sidak over that family, not a bare 2-sigma line.
|
|
|
|
|
double noise = 1.0 / MathSqrt((double)n);
|
|
|
|
|
double bestAbs = 0.0;
|
|
|
|
|
int bestLag = 0, bestCol = -1;
|
|
|
|
|
double tgt[], col[], corr[];
|
|
|
|
|
ArrayResize(tgt, n);
|
|
|
|
|
for(int i = 0; i < n; i++)
|
|
|
|
|
tgt[i] = target[i];
|
|
|
|
|
ArrayResize(col, n);
|
|
|
|
|
for(int f = 0; f < nvars; f++)
|
|
|
|
|
{
|
|
|
|
|
for(int i = 0; i < n; i++)
|
|
|
|
|
col[i] = series[i * nvars + f];
|
|
|
|
|
//--- Both series are mean-centred first: CorrR1D is a raw sum of products, so a column with a
|
|
|
|
|
//--- non-zero mean would return its own mean times the target's at every lag and swamp the
|
|
|
|
|
//--- structure being looked for.
|
|
|
|
|
double mc = MathMean(col), mt = MathMean(tgt);
|
|
|
|
|
double cc[], tc[];
|
|
|
|
|
ArrayResize(cc, n);
|
|
|
|
|
ArrayResize(tc, n);
|
|
|
|
|
double sc = 0.0, st = 0.0;
|
|
|
|
|
for(int i = 0; i < n; i++)
|
|
|
|
|
{
|
|
|
|
|
cc[i] = col[i] - mc;
|
|
|
|
|
tc[i] = tgt[i] - mt;
|
|
|
|
|
sc += cc[i] * cc[i];
|
|
|
|
|
st += tc[i] * tc[i];
|
|
|
|
|
}
|
|
|
|
|
if(sc <= 0.0 || st <= 0.0)
|
|
|
|
|
continue; // a constant column correlates with nothing
|
|
|
|
|
double denom = MathSqrt(sc * st);
|
|
|
|
|
CCorr::CorrR1D(cc, n, tc, n, corr);
|
|
|
|
|
int cn = ArraySize(corr);
|
|
|
|
|
for(int k = 0; k < cn; k++)
|
|
|
|
|
{
|
|
|
|
|
double rr = MathAbs(corr[k]) / denom;
|
|
|
|
|
if(rr > bestAbs)
|
|
|
|
|
{
|
|
|
|
|
bestAbs = rr;
|
|
|
|
|
//--- CorrR1D returns the non-circular correlation with negative lags first; index n-1 is
|
|
|
|
|
//--- lag 0, so this reports the offset in bars with its sign.
|
|
|
|
|
bestLag = k - (n - 1);
|
|
|
|
|
bestCol = f;
|
|
|
|
|
}
|
|
|
|
|
}
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
if(m_data.Stopping())
|
feat(baselines): combining-weight fit, MLP cross-validation, all-lags correlation
Three ALGLIB additions, all measurement-only and all under the existing
Run_Alglib_Baselines switch.
MinBLEIC COMBINING WEIGHTS. The live ensemble weights each member by its
own pooled holdout win rate - a defensible prior, but not a fit, and
nothing has ever asked what mixture minimises error on the bars the
members disagreed about. Two individually-mediocre members wrong in
different places can beat one individually better, and a per-member win
rate cannot express that because it never looks at them jointly.
Solved on the simplex (w >= 0, sum w = 1), which is exactly what
MinBLEIC is for. Non-negative because a negative weight asserts "trade
the opposite of this member", a claim ~60 effective observations cannot
support. Least squares on the signed outcome rather than precision:
precision is a STEP function of the threshold that no gradient method
can walk, and optimising a smooth proxy for a step decision is how
c3daded put every operating point 14pp underwater - so the result is
reported in BOTH currencies, the SSE it minimised and the directional
hit rate the mixture would actually have scored against the equal mix.
If the second does not improve, the first is noise.
This needed data that did not exist: g_ensVoteSum accumulates member
contributions and the sum destroys the decomposition, while
g_ensVoteVoterMask records only WHETHER a member voted, never what.
g_ensVoteMember[] keeps them unsummed. The live arithmetic is untouched.
ENS_MAX_MEMBERS is 8 and deliberately larger than MAX_AI_SIGNALS (5):
independent caps, over-allocating is free, and matching them would make
this array silently short the day the registry grows - a cap that has
already dropped a member once without saying so.
MLPKFoldCVLBFGS. Every baseline row carries a binomial SE, which is the
sampling error of SCORING a fixed model and says nothing about how much
the FIT moves. One LBFGS run from one random start can land anywhere,
and a baseline that cleared or missed the bar on luck of initialisation
reads exactly like one that did it on merit. 3 folds, because each is a
full retrain. LBFGS not LM - LM builds a Hessian over ~7,700 weights.
CCorr ALL-LAGS PROFILE. Added BESIDE the MI lag profile, not instead:
MI catches nonlinear dependence and is the stronger negative, which is
why it settled the verdict - but its per-lag permutation null limits it
to ~20 lags. FFT correlation gets every lag in one O(n log n) pass, so
linear structure parked at lag 300 would surface for free. Different
question, not a replacement. Walks CONTIGUOUS bars, unlike everything
else in this file, because a lag index is meaningless otherwise; both
series are mean-centred first since CorrR1D is a raw sum of products;
and the max over columns x lags is judged against a Sidak family of
exactly that size, not a bare 2-sigma line.
fasttransforms.mqh needed its own include - verified that none of
ap/optimization/statistics/solvers/linalg reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:44:36 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if(bestCol < 0)
|
|
|
|
|
{
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + ": linear lag profile - every column was constant over the window.");
|
feat(baselines): combining-weight fit, MLP cross-validation, all-lags correlation
Three ALGLIB additions, all measurement-only and all under the existing
Run_Alglib_Baselines switch.
MinBLEIC COMBINING WEIGHTS. The live ensemble weights each member by its
own pooled holdout win rate - a defensible prior, but not a fit, and
nothing has ever asked what mixture minimises error on the bars the
members disagreed about. Two individually-mediocre members wrong in
different places can beat one individually better, and a per-member win
rate cannot express that because it never looks at them jointly.
Solved on the simplex (w >= 0, sum w = 1), which is exactly what
MinBLEIC is for. Non-negative because a negative weight asserts "trade
the opposite of this member", a claim ~60 effective observations cannot
support. Least squares on the signed outcome rather than precision:
precision is a STEP function of the threshold that no gradient method
can walk, and optimising a smooth proxy for a step decision is how
c3daded put every operating point 14pp underwater - so the result is
reported in BOTH currencies, the SSE it minimised and the directional
hit rate the mixture would actually have scored against the equal mix.
If the second does not improve, the first is noise.
This needed data that did not exist: g_ensVoteSum accumulates member
contributions and the sum destroys the decomposition, while
g_ensVoteVoterMask records only WHETHER a member voted, never what.
g_ensVoteMember[] keeps them unsummed. The live arithmetic is untouched.
ENS_MAX_MEMBERS is 8 and deliberately larger than MAX_AI_SIGNALS (5):
independent caps, over-allocating is free, and matching them would make
this array silently short the day the registry grows - a cap that has
already dropped a member once without saying so.
MLPKFoldCVLBFGS. Every baseline row carries a binomial SE, which is the
sampling error of SCORING a fixed model and says nothing about how much
the FIT moves. One LBFGS run from one random start can land anywhere,
and a baseline that cleared or missed the bar on luck of initialisation
reads exactly like one that did it on merit. 3 folds, because each is a
full retrain. LBFGS not LM - LM builds a Hessian over ~7,700 weights.
CCorr ALL-LAGS PROFILE. Added BESIDE the MI lag profile, not instead:
MI catches nonlinear dependence and is the stronger negative, which is
why it settled the verdict - but its per-lag permutation null limits it
to ~20 lags. FFT correlation gets every lag in one O(n log n) pass, so
linear structure parked at lag 300 would surface for free. Different
question, not a replacement. Walks CONTIGUOUS bars, unlike everything
else in this file, because a lag index is meaningless otherwise; both
series are mean-centred first since CorrR1D is a raw sum of products;
and the max over columns x lags is judged against a Sidak family of
exactly that size, not a bare 2-sigma line.
fasttransforms.mqh needed its own include - verified that none of
ap/optimization/statistics/solvers/linalg reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:44:36 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
double z = bestAbs / noise;
|
|
|
|
|
double pFamily = SidakFamilyP(z, n * nvars);
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + StringFormat(": linear lag profile - strongest of %d columns x %d lags is column %d at"
|
feat(baselines): combining-weight fit, MLP cross-validation, all-lags correlation
Three ALGLIB additions, all measurement-only and all under the existing
Run_Alglib_Baselines switch.
MinBLEIC COMBINING WEIGHTS. The live ensemble weights each member by its
own pooled holdout win rate - a defensible prior, but not a fit, and
nothing has ever asked what mixture minimises error on the bars the
members disagreed about. Two individually-mediocre members wrong in
different places can beat one individually better, and a per-member win
rate cannot express that because it never looks at them jointly.
Solved on the simplex (w >= 0, sum w = 1), which is exactly what
MinBLEIC is for. Non-negative because a negative weight asserts "trade
the opposite of this member", a claim ~60 effective observations cannot
support. Least squares on the signed outcome rather than precision:
precision is a STEP function of the threshold that no gradient method
can walk, and optimising a smooth proxy for a step decision is how
c3daded put every operating point 14pp underwater - so the result is
reported in BOTH currencies, the SSE it minimised and the directional
hit rate the mixture would actually have scored against the equal mix.
If the second does not improve, the first is noise.
This needed data that did not exist: g_ensVoteSum accumulates member
contributions and the sum destroys the decomposition, while
g_ensVoteVoterMask records only WHETHER a member voted, never what.
g_ensVoteMember[] keeps them unsummed. The live arithmetic is untouched.
ENS_MAX_MEMBERS is 8 and deliberately larger than MAX_AI_SIGNALS (5):
independent caps, over-allocating is free, and matching them would make
this array silently short the day the registry grows - a cap that has
already dropped a member once without saying so.
MLPKFoldCVLBFGS. Every baseline row carries a binomial SE, which is the
sampling error of SCORING a fixed model and says nothing about how much
the FIT moves. One LBFGS run from one random start can land anywhere,
and a baseline that cleared or missed the bar on luck of initialisation
reads exactly like one that did it on merit. 3 folds, because each is a
full retrain. LBFGS not LM - LM builds a Hessian over ~7,700 weights.
CCorr ALL-LAGS PROFILE. Added BESIDE the MI lag profile, not instead:
MI catches nonlinear dependence and is the stronger negative, which is
why it settled the verdict - but its per-lag permutation null limits it
to ~20 lags. FFT correlation gets every lag in one O(n log n) pass, so
linear structure parked at lag 300 would surface for free. Different
question, not a replacement. Walks CONTIGUOUS bars, unlike everything
else in this file, because a lag index is meaningless otherwise; both
series are mean-centred first since CorrR1D is a raw sum of products;
and the max over columns x lags is judged against a Sidak family of
exactly that size, not a bare 2-sigma line.
fasttransforms.mqh needed its own include - verified that none of
ap/optimization/statistics/solvers/linalg reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:44:36 -04:00
|
|
|
" lag %+d, |r| %.4f (%.1f SE of the %.4f no-information band)."
|
|
|
|
|
" Family-wise p %.4f -> %s.",
|
|
|
|
|
nvars, 2 * n - 1, bestCol, bestLag, bestAbs, z, noise, pFamily,
|
|
|
|
|
(pFamily <= DEPLOY_FAMILY_WISE_ALPHA
|
|
|
|
|
? "SURVIVES the best-of-N null - worth a look"
|
|
|
|
|
: "consistent with no linear structure at any lag")));
|
feat(baselines): matrix redundancy and a declustered regime test
Two ALGLIB diagnostics on the pass that already builds the matrix, under
the same Run_Alglib_Baselines switch, so they cost nothing by default
and share one walk of the feature pipeline.
REDUNDANCY (CBaseStat::PearsonCorrM + CPCAnalysis::PCABuildBasis).
The alt-data block already produced this finding once, by hand: 13
features x 16 window slots = 208 inputs spanning ~11.5 effective
dimensions, and the cost was not wasted parameters but a gradient
weighting bias, since batch norm rescales collinear copies without
decorrelating them. This generalises that measurement to every column
instead of the one block someone thought to check.
Run on the ANCHOR BAR'S ROW, not the full window. The window is
m_historyBars near-copies of the same columns at different lags, so
per-bar redundancy is the question worth asking - the lag structure is
what the conv/LSTM stage exists to exploit - and it keeps the eigensolve
at ~60x60 rather than ~960x960.
REGIME STABILITY (CMannWhitneyU::CMannWhitneyUTest) on the net's own
signed margin: its conviction, signed by whether the conviction paid,
older half of the OOS window against newer.
DECLUSTERED FIRST, which is the whole reason this is not a one-line
call. Triple-barrier labels overlap, so consecutive scored bars share
the price action that resolved them and are not independent draws.
Feeding all of them to a rank test yields a p computed on an n it does
not have - the same error label overlap invalidated everywhere else
here. One sample per label horizon costs most of the rows and buys a p
that means something; when too few survive, it says so and declines.
Both report and neither acts. Wiring the regime flag to position sizing
is the obvious next step and the one to take only after watching the
number: run every era on ~60 effective observations, a 0.05 test crosses
by chance regularly, and an auto-derisk on that is a random position
size generator.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:01:34 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
2026-08-22 00:30:14 -04:00
|
|
|
//| HOW WIDE IS THE MATRIX REALLY? Two readings of the same |
|
|
|
|
|
//| question, both on the anchor bar's own feature row. |
|
feat(baselines): matrix redundancy and a declustered regime test
Two ALGLIB diagnostics on the pass that already builds the matrix, under
the same Run_Alglib_Baselines switch, so they cost nothing by default
and share one walk of the feature pipeline.
REDUNDANCY (CBaseStat::PearsonCorrM + CPCAnalysis::PCABuildBasis).
The alt-data block already produced this finding once, by hand: 13
features x 16 window slots = 208 inputs spanning ~11.5 effective
dimensions, and the cost was not wasted parameters but a gradient
weighting bias, since batch norm rescales collinear copies without
decorrelating them. This generalises that measurement to every column
instead of the one block someone thought to check.
Run on the ANCHOR BAR'S ROW, not the full window. The window is
m_historyBars near-copies of the same columns at different lags, so
per-bar redundancy is the question worth asking - the lag structure is
what the conv/LSTM stage exists to exploit - and it keeps the eigensolve
at ~60x60 rather than ~960x960.
REGIME STABILITY (CMannWhitneyU::CMannWhitneyUTest) on the net's own
signed margin: its conviction, signed by whether the conviction paid,
older half of the OOS window against newer.
DECLUSTERED FIRST, which is the whole reason this is not a one-line
call. Triple-barrier labels overlap, so consecutive scored bars share
the price action that resolved them and are not independent draws.
Feeding all of them to a rank test yields a p computed on an n it does
not have - the same error label overlap invalidated everywhere else
here. One sample per label horizon costs most of the rows and buys a p
that means something; when too few survive, it says so and declines.
Both report and neither acts. Wiring the regime flag to position sizing
is the obvious next step and the one to take only after watching the
number: run every era on ~60 effective observations, a 0.05 test crosses
by chance regularly, and an auto-derisk on that is a random position
size generator.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:01:34 -04:00
|
|
|
//+------------------------------------------------------------------+
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
void CBaselineComparator::ReportFeatureRedundancy(CMatrixDouble &rows, const int n)
|
feat(baselines): matrix redundancy and a declustered regime test
Two ALGLIB diagnostics on the pass that already builds the matrix, under
the same Run_Alglib_Baselines switch, so they cost nothing by default
and share one walk of the feature pipeline.
REDUNDANCY (CBaseStat::PearsonCorrM + CPCAnalysis::PCABuildBasis).
The alt-data block already produced this finding once, by hand: 13
features x 16 window slots = 208 inputs spanning ~11.5 effective
dimensions, and the cost was not wasted parameters but a gradient
weighting bias, since batch norm rescales collinear copies without
decorrelating them. This generalises that measurement to every column
instead of the one block someone thought to check.
Run on the ANCHOR BAR'S ROW, not the full window. The window is
m_historyBars near-copies of the same columns at different lags, so
per-bar redundancy is the question worth asking - the lag structure is
what the conv/LSTM stage exists to exploit - and it keeps the eigensolve
at ~60x60 rather than ~960x960.
REGIME STABILITY (CMannWhitneyU::CMannWhitneyUTest) on the net's own
signed margin: its conviction, signed by whether the conviction paid,
older half of the OOS window against newer.
DECLUSTERED FIRST, which is the whole reason this is not a one-line
call. Triple-barrier labels overlap, so consecutive scored bars share
the price action that resolved them and are not independent draws.
Feeding all of them to a rank test yields a p computed on an n it does
not have - the same error label overlap invalidated everywhere else
here. One sample per label horizon costs most of the rows and buys a p
that means something; when too few survive, it says so and declines.
Both report and neither acts. Wiring the regime flag to position sizing
is the obvious next step and the one to take only after watching the
number: run every era on ~60 effective observations, a 0.05 test crosses
by chance regularly, and an auto-derisk on that is a random position
size generator.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:01:34 -04:00
|
|
|
{
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
int nvars = m_data.FeaturesPerBar();
|
feat(baselines): matrix redundancy and a declustered regime test
Two ALGLIB diagnostics on the pass that already builds the matrix, under
the same Run_Alglib_Baselines switch, so they cost nothing by default
and share one walk of the feature pipeline.
REDUNDANCY (CBaseStat::PearsonCorrM + CPCAnalysis::PCABuildBasis).
The alt-data block already produced this finding once, by hand: 13
features x 16 window slots = 208 inputs spanning ~11.5 effective
dimensions, and the cost was not wasted parameters but a gradient
weighting bias, since batch norm rescales collinear copies without
decorrelating them. This generalises that measurement to every column
instead of the one block someone thought to check.
Run on the ANCHOR BAR'S ROW, not the full window. The window is
m_historyBars near-copies of the same columns at different lags, so
per-bar redundancy is the question worth asking - the lag structure is
what the conv/LSTM stage exists to exploit - and it keeps the eigensolve
at ~60x60 rather than ~960x960.
REGIME STABILITY (CMannWhitneyU::CMannWhitneyUTest) on the net's own
signed margin: its conviction, signed by whether the conviction paid,
older half of the OOS window against newer.
DECLUSTERED FIRST, which is the whole reason this is not a one-line
call. Triple-barrier labels overlap, so consecutive scored bars share
the price action that resolved them and are not independent draws.
Feeding all of them to a rank test yields a p computed on an n it does
not have - the same error label overlap invalidated everywhere else
here. One sample per label horizon costs most of the rows and buys a p
that means something; when too few survive, it says so and declines.
Both report and neither acts. Wiring the regime flag to position sizing
is the obvious next step and the one to take only after watching the
number: run every era on ~60 effective observations, a 0.05 test crosses
by chance regularly, and an auto-derisk on that is a random position
size generator.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:01:34 -04:00
|
|
|
if(n < BASELINE_MIN_ROWS || nvars < 2)
|
|
|
|
|
return;
|
|
|
|
|
//--- PAIRWISE. The worst offender by absolute correlation, plus how many pairs are effectively
|
|
|
|
|
//--- one column wearing two names.
|
|
|
|
|
CMatrixDouble corr;
|
|
|
|
|
if(CBaseStat::PearsonCorrM(rows, n, nvars, corr))
|
|
|
|
|
{
|
|
|
|
|
int pairs = 0, wi = -1, wj = -1;
|
|
|
|
|
double worst = 0.0;
|
|
|
|
|
for(int i = 0; i < nvars; i++)
|
|
|
|
|
for(int j = i + 1; j < nvars; j++)
|
|
|
|
|
{
|
|
|
|
|
double r = MathAbs(corr.Get(i, j));
|
|
|
|
|
if(!MathIsValidNumber(r))
|
|
|
|
|
continue;
|
|
|
|
|
if(r >= BASELINE_COLLINEAR_ABS_R)
|
|
|
|
|
pairs++;
|
|
|
|
|
if(r > worst)
|
|
|
|
|
{
|
|
|
|
|
worst = r;
|
|
|
|
|
wi = i;
|
|
|
|
|
wj = j;
|
|
|
|
|
}
|
|
|
|
|
}
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + StringFormat(": matrix redundancy - %d of %d column pairs correlate above %.2f;"
|
feat(baselines): matrix redundancy and a declustered regime test
Two ALGLIB diagnostics on the pass that already builds the matrix, under
the same Run_Alglib_Baselines switch, so they cost nothing by default
and share one walk of the feature pipeline.
REDUNDANCY (CBaseStat::PearsonCorrM + CPCAnalysis::PCABuildBasis).
The alt-data block already produced this finding once, by hand: 13
features x 16 window slots = 208 inputs spanning ~11.5 effective
dimensions, and the cost was not wasted parameters but a gradient
weighting bias, since batch norm rescales collinear copies without
decorrelating them. This generalises that measurement to every column
instead of the one block someone thought to check.
Run on the ANCHOR BAR'S ROW, not the full window. The window is
m_historyBars near-copies of the same columns at different lags, so
per-bar redundancy is the question worth asking - the lag structure is
what the conv/LSTM stage exists to exploit - and it keeps the eigensolve
at ~60x60 rather than ~960x960.
REGIME STABILITY (CMannWhitneyU::CMannWhitneyUTest) on the net's own
signed margin: its conviction, signed by whether the conviction paid,
older half of the OOS window against newer.
DECLUSTERED FIRST, which is the whole reason this is not a one-line
call. Triple-barrier labels overlap, so consecutive scored bars share
the price action that resolved them and are not independent draws.
Feeding all of them to a rank test yields a p computed on an n it does
not have - the same error label overlap invalidated everywhere else
here. One sample per label horizon costs most of the rows and buys a p
that means something; when too few survive, it says so and declines.
Both report and neither acts. Wiring the regime flag to position sizing
is the obvious next step and the one to take only after watching the
number: run every era on ~60 effective observations, a 0.05 test crosses
by chance regularly, and an auto-derisk on that is a random position
size generator.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:01:34 -04:00
|
|
|
" worst is columns %d/%d at |r| %.3f.",
|
|
|
|
|
pairs, nvars * (nvars - 1) / 2, BASELINE_COLLINEAR_ABS_R, wi, wj, worst));
|
|
|
|
|
}
|
|
|
|
|
//--- EFFECTIVE WIDTH. PCABuildBasis returns the component variances in decreasing order, so the
|
|
|
|
|
//--- count needed to reach BASELINE_PCA_VAR_SHARE is the number of directions the columns really
|
|
|
|
|
//--- span. Far cheaper here than on the full window: nvars^2, not (historyBars*nvars)^2.
|
|
|
|
|
int info = 0;
|
|
|
|
|
double s2[];
|
|
|
|
|
CMatrixDouble basis;
|
|
|
|
|
CPCAnalysis::PCABuildBasis(rows, n, nvars, info, s2, basis);
|
|
|
|
|
if(info <= 0 || ArraySize(s2) < nvars)
|
|
|
|
|
{
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + StringFormat(": matrix redundancy - PCA did not converge (info %d).", info));
|
feat(baselines): matrix redundancy and a declustered regime test
Two ALGLIB diagnostics on the pass that already builds the matrix, under
the same Run_Alglib_Baselines switch, so they cost nothing by default
and share one walk of the feature pipeline.
REDUNDANCY (CBaseStat::PearsonCorrM + CPCAnalysis::PCABuildBasis).
The alt-data block already produced this finding once, by hand: 13
features x 16 window slots = 208 inputs spanning ~11.5 effective
dimensions, and the cost was not wasted parameters but a gradient
weighting bias, since batch norm rescales collinear copies without
decorrelating them. This generalises that measurement to every column
instead of the one block someone thought to check.
Run on the ANCHOR BAR'S ROW, not the full window. The window is
m_historyBars near-copies of the same columns at different lags, so
per-bar redundancy is the question worth asking - the lag structure is
what the conv/LSTM stage exists to exploit - and it keeps the eigensolve
at ~60x60 rather than ~960x960.
REGIME STABILITY (CMannWhitneyU::CMannWhitneyUTest) on the net's own
signed margin: its conviction, signed by whether the conviction paid,
older half of the OOS window against newer.
DECLUSTERED FIRST, which is the whole reason this is not a one-line
call. Triple-barrier labels overlap, so consecutive scored bars share
the price action that resolved them and are not independent draws.
Feeding all of them to a rank test yields a p computed on an n it does
not have - the same error label overlap invalidated everywhere else
here. One sample per label horizon costs most of the rows and buys a p
that means something; when too few survive, it says so and declines.
Both report and neither acts. Wiring the regime flag to position sizing
is the obvious next step and the one to take only after watching the
number: run every era on ~60 effective observations, a 0.05 test crosses
by chance regularly, and an auto-derisk on that is a random position
size generator.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:01:34 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
double total = 0.0;
|
|
|
|
|
for(int i = 0; i < nvars; i++)
|
|
|
|
|
total += MathMax(0.0, s2[i]);
|
|
|
|
|
if(total <= 0.0)
|
|
|
|
|
return;
|
|
|
|
|
double cum = 0.0;
|
|
|
|
|
int need = nvars;
|
|
|
|
|
for(int i = 0; i < nvars; i++)
|
|
|
|
|
{
|
|
|
|
|
cum += MathMax(0.0, s2[i]);
|
|
|
|
|
if(cum / total >= BASELINE_PCA_VAR_SHARE)
|
|
|
|
|
{
|
|
|
|
|
need = i + 1;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + StringFormat(": matrix redundancy - %d of %d columns carry %.0f%% of the variance"
|
feat(baselines): matrix redundancy and a declustered regime test
Two ALGLIB diagnostics on the pass that already builds the matrix, under
the same Run_Alglib_Baselines switch, so they cost nothing by default
and share one walk of the feature pipeline.
REDUNDANCY (CBaseStat::PearsonCorrM + CPCAnalysis::PCABuildBasis).
The alt-data block already produced this finding once, by hand: 13
features x 16 window slots = 208 inputs spanning ~11.5 effective
dimensions, and the cost was not wasted parameters but a gradient
weighting bias, since batch norm rescales collinear copies without
decorrelating them. This generalises that measurement to every column
instead of the one block someone thought to check.
Run on the ANCHOR BAR'S ROW, not the full window. The window is
m_historyBars near-copies of the same columns at different lags, so
per-bar redundancy is the question worth asking - the lag structure is
what the conv/LSTM stage exists to exploit - and it keeps the eigensolve
at ~60x60 rather than ~960x960.
REGIME STABILITY (CMannWhitneyU::CMannWhitneyUTest) on the net's own
signed margin: its conviction, signed by whether the conviction paid,
older half of the OOS window against newer.
DECLUSTERED FIRST, which is the whole reason this is not a one-line
call. Triple-barrier labels overlap, so consecutive scored bars share
the price action that resolved them and are not independent draws.
Feeding all of them to a rank test yields a p computed on an n it does
not have - the same error label overlap invalidated everywhere else
here. One sample per label horizon costs most of the rows and buys a p
that means something; when too few survive, it says so and declines.
Both report and neither acts. Wiring the regime flag to position sizing
is the obvious next step and the one to take only after watching the
number: run every era on ~60 effective observations, a 0.05 test crosses
by chance regularly, and an auto-derisk on that is a random position
size generator.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:01:34 -04:00
|
|
|
" (top component alone %.0f%%). The window multiplies this by %d lags,"
|
|
|
|
|
" so the net sees ~%d effective inputs, not %d.",
|
|
|
|
|
need, nvars, BASELINE_PCA_VAR_SHARE * 100.0, 100.0 * s2[0] / total,
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
m_data.HistoryBars(), need * m_data.HistoryBars(),
|
|
|
|
|
m_data.HistoryBars() * nvars));
|
feat(baselines): matrix redundancy and a declustered regime test
Two ALGLIB diagnostics on the pass that already builds the matrix, under
the same Run_Alglib_Baselines switch, so they cost nothing by default
and share one walk of the feature pipeline.
REDUNDANCY (CBaseStat::PearsonCorrM + CPCAnalysis::PCABuildBasis).
The alt-data block already produced this finding once, by hand: 13
features x 16 window slots = 208 inputs spanning ~11.5 effective
dimensions, and the cost was not wasted parameters but a gradient
weighting bias, since batch norm rescales collinear copies without
decorrelating them. This generalises that measurement to every column
instead of the one block someone thought to check.
Run on the ANCHOR BAR'S ROW, not the full window. The window is
m_historyBars near-copies of the same columns at different lags, so
per-bar redundancy is the question worth asking - the lag structure is
what the conv/LSTM stage exists to exploit - and it keeps the eigensolve
at ~60x60 rather than ~960x960.
REGIME STABILITY (CMannWhitneyU::CMannWhitneyUTest) on the net's own
signed margin: its conviction, signed by whether the conviction paid,
older half of the OOS window against newer.
DECLUSTERED FIRST, which is the whole reason this is not a one-line
call. Triple-barrier labels overlap, so consecutive scored bars share
the price action that resolved them and are not independent draws.
Feeding all of them to a rank test yields a p computed on an n it does
not have - the same error label overlap invalidated everywhere else
here. One sample per label horizon costs most of the rows and buys a p
that means something; when too few survive, it says so and declines.
Both report and neither acts. Wiring the regime flag to position sizing
is the obvious next step and the one to take only after watching the
number: run every era on ~60 effective observations, a 0.05 test crosses
by chance regularly, and an auto-derisk on that is a random position
size generator.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:01:34 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
2026-08-22 00:30:14 -04:00
|
|
|
//| HAS THE EDGE MOVED? Mann-Whitney U on the net's own signed |
|
|
|
|
|
//| margin (conviction, signed by whether the conviction paid), |
|
|
|
|
|
//| older half of the OOS window against newer. |
|
feat(baselines): matrix redundancy and a declustered regime test
Two ALGLIB diagnostics on the pass that already builds the matrix, under
the same Run_Alglib_Baselines switch, so they cost nothing by default
and share one walk of the feature pipeline.
REDUNDANCY (CBaseStat::PearsonCorrM + CPCAnalysis::PCABuildBasis).
The alt-data block already produced this finding once, by hand: 13
features x 16 window slots = 208 inputs spanning ~11.5 effective
dimensions, and the cost was not wasted parameters but a gradient
weighting bias, since batch norm rescales collinear copies without
decorrelating them. This generalises that measurement to every column
instead of the one block someone thought to check.
Run on the ANCHOR BAR'S ROW, not the full window. The window is
m_historyBars near-copies of the same columns at different lags, so
per-bar redundancy is the question worth asking - the lag structure is
what the conv/LSTM stage exists to exploit - and it keeps the eigensolve
at ~60x60 rather than ~960x960.
REGIME STABILITY (CMannWhitneyU::CMannWhitneyUTest) on the net's own
signed margin: its conviction, signed by whether the conviction paid,
older half of the OOS window against newer.
DECLUSTERED FIRST, which is the whole reason this is not a one-line
call. Triple-barrier labels overlap, so consecutive scored bars share
the price action that resolved them and are not independent draws.
Feeding all of them to a rank test yields a p computed on an n it does
not have - the same error label overlap invalidated everywhere else
here. One sample per label horizon costs most of the rows and buys a p
that means something; when too few survive, it says so and declines.
Both report and neither acts. Wiring the regime flag to position sizing
is the obvious next step and the one to take only after watching the
number: run every era on ~60 effective observations, a 0.05 test crosses
by chance regularly, and an auto-derisk on that is a random position
size generator.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:01:34 -04:00
|
|
|
//+------------------------------------------------------------------+
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
void CBaselineComparator::ReportRegimeStability(double &margins[], int &marginBars[])
|
feat(baselines): matrix redundancy and a declustered regime test
Two ALGLIB diagnostics on the pass that already builds the matrix, under
the same Run_Alglib_Baselines switch, so they cost nothing by default
and share one walk of the feature pipeline.
REDUNDANCY (CBaseStat::PearsonCorrM + CPCAnalysis::PCABuildBasis).
The alt-data block already produced this finding once, by hand: 13
features x 16 window slots = 208 inputs spanning ~11.5 effective
dimensions, and the cost was not wasted parameters but a gradient
weighting bias, since batch norm rescales collinear copies without
decorrelating them. This generalises that measurement to every column
instead of the one block someone thought to check.
Run on the ANCHOR BAR'S ROW, not the full window. The window is
m_historyBars near-copies of the same columns at different lags, so
per-bar redundancy is the question worth asking - the lag structure is
what the conv/LSTM stage exists to exploit - and it keeps the eigensolve
at ~60x60 rather than ~960x960.
REGIME STABILITY (CMannWhitneyU::CMannWhitneyUTest) on the net's own
signed margin: its conviction, signed by whether the conviction paid,
older half of the OOS window against newer.
DECLUSTERED FIRST, which is the whole reason this is not a one-line
call. Triple-barrier labels overlap, so consecutive scored bars share
the price action that resolved them and are not independent draws.
Feeding all of them to a rank test yields a p computed on an n it does
not have - the same error label overlap invalidated everywhere else
here. One sample per label horizon costs most of the rows and buys a p
that means something; when too few survive, it says so and declines.
Both report and neither acts. Wiring the regime flag to position sizing
is the obvious next step and the one to take only after watching the
number: run every era on ~60 effective observations, a 0.05 test crosses
by chance regularly, and an auto-derisk on that is a random position
size generator.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:01:34 -04:00
|
|
|
{
|
|
|
|
|
int n = ArraySize(margins);
|
|
|
|
|
if(n < 2 * BASELINE_MIN_REGIME_N)
|
|
|
|
|
return;
|
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
|
|
|
//--- One sample per label resolution lag. These are MQL5 timeseries indices, so the array ascends
|
|
|
|
|
//--- in index while descending in TIME - element 0 is the newest scored call.
|
|
|
|
|
int gap = (int)MathMax(m_data.LabelResolutionBars(), 1);
|
feat(baselines): matrix redundancy and a declustered regime test
Two ALGLIB diagnostics on the pass that already builds the matrix, under
the same Run_Alglib_Baselines switch, so they cost nothing by default
and share one walk of the feature pipeline.
REDUNDANCY (CBaseStat::PearsonCorrM + CPCAnalysis::PCABuildBasis).
The alt-data block already produced this finding once, by hand: 13
features x 16 window slots = 208 inputs spanning ~11.5 effective
dimensions, and the cost was not wasted parameters but a gradient
weighting bias, since batch norm rescales collinear copies without
decorrelating them. This generalises that measurement to every column
instead of the one block someone thought to check.
Run on the ANCHOR BAR'S ROW, not the full window. The window is
m_historyBars near-copies of the same columns at different lags, so
per-bar redundancy is the question worth asking - the lag structure is
what the conv/LSTM stage exists to exploit - and it keeps the eigensolve
at ~60x60 rather than ~960x960.
REGIME STABILITY (CMannWhitneyU::CMannWhitneyUTest) on the net's own
signed margin: its conviction, signed by whether the conviction paid,
older half of the OOS window against newer.
DECLUSTERED FIRST, which is the whole reason this is not a one-line
call. Triple-barrier labels overlap, so consecutive scored bars share
the price action that resolved them and are not independent draws.
Feeding all of them to a rank test yields a p computed on an n it does
not have - the same error label overlap invalidated everywhere else
here. One sample per label horizon costs most of the rows and buys a p
that means something; when too few survive, it says so and declines.
Both report and neither acts. Wiring the regime flag to position sizing
is the obvious next step and the one to take only after watching the
number: run every era on ~60 effective observations, a 0.05 test crosses
by chance regularly, and an auto-derisk on that is a random position
size generator.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:01:34 -04:00
|
|
|
double kept[];
|
|
|
|
|
ArrayResize(kept, n);
|
|
|
|
|
int m = 0, lastBar = -1;
|
|
|
|
|
for(int i = 0; i < n; i++)
|
|
|
|
|
{
|
|
|
|
|
if(lastBar >= 0 && MathAbs(marginBars[i] - lastBar) < gap)
|
|
|
|
|
continue;
|
|
|
|
|
kept[m++] = margins[i];
|
|
|
|
|
lastBar = marginBars[i];
|
|
|
|
|
}
|
|
|
|
|
if(m < 2 * BASELINE_MIN_REGIME_N)
|
|
|
|
|
{
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + StringFormat(": regime test skipped - %d scored calls decluster to only %d"
|
feat(baselines): matrix redundancy and a declustered regime test
Two ALGLIB diagnostics on the pass that already builds the matrix, under
the same Run_Alglib_Baselines switch, so they cost nothing by default
and share one walk of the feature pipeline.
REDUNDANCY (CBaseStat::PearsonCorrM + CPCAnalysis::PCABuildBasis).
The alt-data block already produced this finding once, by hand: 13
features x 16 window slots = 208 inputs spanning ~11.5 effective
dimensions, and the cost was not wasted parameters but a gradient
weighting bias, since batch norm rescales collinear copies without
decorrelating them. This generalises that measurement to every column
instead of the one block someone thought to check.
Run on the ANCHOR BAR'S ROW, not the full window. The window is
m_historyBars near-copies of the same columns at different lags, so
per-bar redundancy is the question worth asking - the lag structure is
what the conv/LSTM stage exists to exploit - and it keeps the eigensolve
at ~60x60 rather than ~960x960.
REGIME STABILITY (CMannWhitneyU::CMannWhitneyUTest) on the net's own
signed margin: its conviction, signed by whether the conviction paid,
older half of the OOS window against newer.
DECLUSTERED FIRST, which is the whole reason this is not a one-line
call. Triple-barrier labels overlap, so consecutive scored bars share
the price action that resolved them and are not independent draws.
Feeding all of them to a rank test yields a p computed on an n it does
not have - the same error label overlap invalidated everywhere else
here. One sample per label horizon costs most of the rows and buys a p
that means something; when too few survive, it says so and declines.
Both report and neither acts. Wiring the regime flag to position sizing
is the obvious next step and the one to take only after watching the
number: run every era on ~60 effective observations, a 0.05 test crosses
by chance regularly, and an auto-derisk on that is a random position
size generator.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:01:34 -04:00
|
|
|
" independent samples at a %d-bar label horizon, below the %d per half"
|
|
|
|
|
" the test needs.", n, m, gap, BASELINE_MIN_REGIME_N));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
//--- kept[] is newest-first, so the SECOND half is the older window.
|
|
|
|
|
int half = m / 2;
|
|
|
|
|
double recent[], older[];
|
|
|
|
|
ArrayResize(recent, half);
|
|
|
|
|
ArrayResize(older, m - half);
|
|
|
|
|
for(int i = 0; i < half; i++)
|
|
|
|
|
recent[i] = kept[i];
|
|
|
|
|
for(int i = half; i < m; i++)
|
|
|
|
|
older[i - half] = kept[i];
|
|
|
|
|
double both = 1.0, left = 1.0, right = 1.0;
|
|
|
|
|
CMannWhitneyU::CMannWhitneyUTest(older, m - half, recent, half, both, left, right);
|
|
|
|
|
double meanOld = MathMean(older), meanNew = MathMean(recent);
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + StringFormat(": regime test - signed margin over %d independent OOS calls"
|
feat(baselines): matrix redundancy and a declustered regime test
Two ALGLIB diagnostics on the pass that already builds the matrix, under
the same Run_Alglib_Baselines switch, so they cost nothing by default
and share one walk of the feature pipeline.
REDUNDANCY (CBaseStat::PearsonCorrM + CPCAnalysis::PCABuildBasis).
The alt-data block already produced this finding once, by hand: 13
features x 16 window slots = 208 inputs spanning ~11.5 effective
dimensions, and the cost was not wasted parameters but a gradient
weighting bias, since batch norm rescales collinear copies without
decorrelating them. This generalises that measurement to every column
instead of the one block someone thought to check.
Run on the ANCHOR BAR'S ROW, not the full window. The window is
m_historyBars near-copies of the same columns at different lags, so
per-bar redundancy is the question worth asking - the lag structure is
what the conv/LSTM stage exists to exploit - and it keeps the eigensolve
at ~60x60 rather than ~960x960.
REGIME STABILITY (CMannWhitneyU::CMannWhitneyUTest) on the net's own
signed margin: its conviction, signed by whether the conviction paid,
older half of the OOS window against newer.
DECLUSTERED FIRST, which is the whole reason this is not a one-line
call. Triple-barrier labels overlap, so consecutive scored bars share
the price action that resolved them and are not independent draws.
Feeding all of them to a rank test yields a p computed on an n it does
not have - the same error label overlap invalidated everywhere else
here. One sample per label horizon costs most of the rows and buys a p
that means something; when too few survive, it says so and declines.
Both report and neither acts. Wiring the regime flag to position sizing
is the obvious next step and the one to take only after watching the
number: run every era on ~60 effective observations, a 0.05 test crosses
by chance regularly, and an auto-derisk on that is a random position
size generator.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:01:34 -04:00
|
|
|
" (%d older / %d newer, declustered from %d at a %d-bar horizon):"
|
|
|
|
|
" mean %+.3f -> %+.3f, Mann-Whitney p %.4f -> %s.",
|
|
|
|
|
m, m - half, half, n, gap, meanOld, meanNew, both,
|
|
|
|
|
(both <= 0.05 ? (meanNew < meanOld ? "DISTRIBUTION SHIFTED, and downward"
|
|
|
|
|
: "distribution shifted, upward")
|
|
|
|
|
: "no detectable shift")));
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
2026-08-22 00:30:14 -04:00
|
|
|
//| One baseline's verdict, through the deploy gate's arithmetic. |
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
//+------------------------------------------------------------------+
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
void CBaselineComparator::ReportBaselineModel(const string label, const int calls, const int hits,
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
const int scored, const double chancePct)
|
|
|
|
|
{
|
|
|
|
|
if(calls <= 0)
|
|
|
|
|
{
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + StringFormat(": baseline %s - abstained on all %d OOS windows. No edge measurable.",
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
label, scored));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
double precPct = 100.0 * hits / calls;
|
diag(baselines): turn the forest/linear comparison on, and deflate its SE
Run_Alglib_Baselines = true. It answers the question the campaign is
actually stuck on - is a flat result the architecture or the matrix - by
fitting an Alglib forest, MLP and OLS on the net's OWN windows, labels,
split and gate arithmetic. Nothing trades on it and no model is saved.
Not in BuildModelFingerprint, so this does NOT re-key anything: no
retrain, it can go out with the next compile. One-shot per chart
(m_baselineDone latches first, and g_ensembleChartBaselinesDone stops
the other three members re-measuring the identical fit), bounded by
BASELINE_BUDGET_MS = 45 s, which it spends frozen because the EA is
single threaded - it says so when it stops early.
FIXED WHILE TURNING IT ON: ReportBaselineModel sized its SE on RAW call
count while the NN's own DEPLOY BAR deflates by the mean label lifespan.
The whole point of this line is a like-for-like comparison against the
net on the same windows, and it was holding the baseline to the more
permissive standard - which is how the last forest run's +8.4pp read as
significant when it was +1.0 SE after deflation. Now uses
EffectiveSampleSize and prints how many independent calls that leaves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 10:47:25 -04:00
|
|
|
//--- DEFLATED, like the NN's own DEPLOY BAR is. These calls are consecutive bars sharing
|
|
|
|
|
//--- overlapping labels, not independent trades, so raw n understates the SE by ~sqrt(L) - and
|
|
|
|
|
//--- this line exists to be compared against the net measured on the SAME windows. Judging the
|
|
|
|
|
//--- baseline on the more permissive standard is how a forest at +1.0 SE reads as +3.6.
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
double effN = m_data.EffectiveSampleSize((double)calls);
|
diag(baselines): turn the forest/linear comparison on, and deflate its SE
Run_Alglib_Baselines = true. It answers the question the campaign is
actually stuck on - is a flat result the architecture or the matrix - by
fitting an Alglib forest, MLP and OLS on the net's OWN windows, labels,
split and gate arithmetic. Nothing trades on it and no model is saved.
Not in BuildModelFingerprint, so this does NOT re-key anything: no
retrain, it can go out with the next compile. One-shot per chart
(m_baselineDone latches first, and g_ensembleChartBaselinesDone stops
the other three members re-measuring the identical fit), bounded by
BASELINE_BUDGET_MS = 45 s, which it spends frozen because the EA is
single threaded - it says so when it stops early.
FIXED WHILE TURNING IT ON: ReportBaselineModel sized its SE on RAW call
count while the NN's own DEPLOY BAR deflates by the mean label lifespan.
The whole point of this line is a like-for-like comparison against the
net on the same windows, and it was holding the baseline to the more
permissive standard - which is how the last forest run's +8.4pp read as
significant when it was +1.0 SE after deflation. Now uses
EffectiveSampleSize and prints how many independent calls that leaves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 10:47:25 -04:00
|
|
|
double se = BinomialSEPct(chancePct / 100.0, effN);
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
if(se <= 0.0)
|
|
|
|
|
{
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + StringFormat(": baseline %s - %.1f%% on %d calls, but chance %.1f%% is degenerate"
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
" so no test applies.", label, precPct, calls, chancePct));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
double z = (precPct - chancePct) / se;
|
|
|
|
|
double pFamily = SidakFamilyP(z, BASELINE_MODELS_TRIED);
|
refactor(baselines): the first real module - a class, not an #included partial
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:00:18 -04:00
|
|
|
Print(m_data.Id() + StringFormat(": baseline %s - %.1f%% precision on %d calls (%.0f%% coverage, worth"
|
diag(baselines): turn the forest/linear comparison on, and deflate its SE
Run_Alglib_Baselines = true. It answers the question the campaign is
actually stuck on - is a flat result the architecture or the matrix - by
fitting an Alglib forest, MLP and OLS on the net's OWN windows, labels,
split and gate arithmetic. Nothing trades on it and no model is saved.
Not in BuildModelFingerprint, so this does NOT re-key anything: no
retrain, it can go out with the next compile. One-shot per chart
(m_baselineDone latches first, and g_ensembleChartBaselinesDone stops
the other three members re-measuring the identical fit), bounded by
BASELINE_BUDGET_MS = 45 s, which it spends frozen because the EA is
single threaded - it says so when it stops early.
FIXED WHILE TURNING IT ON: ReportBaselineModel sized its SE on RAW call
count while the NN's own DEPLOY BAR deflates by the mean label lifespan.
The whole point of this line is a like-for-like comparison against the
net on the same windows, and it was holding the baseline to the more
permissive standard - which is how the last forest run's +8.4pp read as
significant when it was +1.0 SE after deflation. Now uses
EffectiveSampleSize and prints how many independent calls that leaves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 10:47:25 -04:00
|
|
|
" %.0f INDEPENDENT ones after the label overlap) vs %.1f%% chance | edge"
|
|
|
|
|
" %+.1fpp = %+.2f SE | family-wise p %.4f -> %s.",
|
|
|
|
|
label, precPct, calls, 100.0 * calls / scored, effN, chancePct,
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
precPct - chancePct, z, pFamily,
|
|
|
|
|
(pFamily <= DEPLOY_FAMILY_WISE_ALPHA ? "CLEARS the deploy gate"
|
|
|
|
|
: "does not clear")));
|
|
|
|
|
}
|
2026-08-20 00:34:55 -04:00
|
|
|
|
feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
|
|
|
#endif
|