Warrior_EA/Expert/Training/BaselineComparator.mqh

915 lines
45 KiB
MQL5
Raw Permalink Normal View History

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
//--- ONCE-PER-CHART, same doctrine as g_ensembleChartMiReportDone (AutoTune.mqh) and for a stronger
//--- reason. Four un-chunked forest+MLP+OLS fits over a ~1000-column, 4000-row design, to print the
//--- same numbers four times.
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
//--- 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
fix(baselines): the budget said 45 s and the log said 548.8 s, because a boundary check cannot stop a phase in flight Yesterday's fix predicted the cross-validation's cost and declined it before starting, then left the linear fit to a plain boundary check. The 17:01 run says exactly what that was worth: ALGLIB MLP trained in 12.1 s ... NOTE: 6435 weights against 995 rows cross-validation SKIPPED - 3 folds x 12.1 s = ~36.3 s predicted against 31.9 s left Alglib baselines stopping before the OOS scoring - 548.8 s spent of a 45 s budget Everything up to the linear fit obeyed the budget. LRBuild then held the thread for ~535 s in one uninterruptible call and the check fired afterwards, on a decision that was already made. The same block shows up in the training timer as 'SLOW ERA heartbeat - net fwd/back 3.9s | everything else 566.2s', and in three members re-arming a study event that never arrived. LRBuild solves a (width+1)^2 normal-equation system: the cost grows as width^3 and ignores the row count entirely, so the row cap that fixed the forest and the MLP does nothing here. Predicted from that measurement and declined before it starts, like the CV. Second, independent reason to decline it: at 800 columns against 1000 rows the normal equations are singular, so any coefficients returned are one arbitrary solution of infinitely many. That fit would have been printed as a baseline while carrying no information. Both grounds are checked, and the log says which one applied. The OOS scoring - the phase that answers the question the suite exists for - has now failed to run twice for two different reasons. It is the first thing to check on the next attempt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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
//--- 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.
fix(baselines): the budget said 45 s and the log said 548.8 s, because a boundary check cannot stop a phase in flight Yesterday's fix predicted the cross-validation's cost and declined it before starting, then left the linear fit to a plain boundary check. The 17:01 run says exactly what that was worth: ALGLIB MLP trained in 12.1 s ... NOTE: 6435 weights against 995 rows cross-validation SKIPPED - 3 folds x 12.1 s = ~36.3 s predicted against 31.9 s left Alglib baselines stopping before the OOS scoring - 548.8 s spent of a 45 s budget Everything up to the linear fit obeyed the budget. LRBuild then held the thread for ~535 s in one uninterruptible call and the check fired afterwards, on a decision that was already made. The same block shows up in the training timer as 'SLOW ERA heartbeat - net fwd/back 3.9s | everything else 566.2s', and in three members re-arming a study event that never arrived. LRBuild solves a (width+1)^2 normal-equation system: the cost grows as width^3 and ignores the row count entirely, so the row cap that fixed the forest and the MLP does nothing here. Predicted from that measurement and declined before it starts, like the CV. Second, independent reason to decline it: at 800 columns against 1000 rows the normal equations are singular, so any coefficients returned are one arbitrary solution of infinitely many. That fit would have been printed as a baseline while carrying no information. Both grounds are checked, and the log says which one applied. The OOS scoring - the phase that answers the question the suite exists for - has now failed to run twice for two different reasons. It is the first thing to check on the next attempt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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
//--- 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
//--- 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
//+------------------------------------------------------------------+
//| 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)
{
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"
" 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"))
feat(baselines): geometry-drift check on the derived stop DeriveBarrierGeometry() reads the stop off a quantile of the adverse excursions in the IS region ONLY - correctly, since a geometry chosen with the holdout in view has used the holdout for selection and it stops being a holdout. The cost of that correct choice is that nothing ever checked whether the distribution it measured still holds on the bars the model actually trades. If adverse excursions run wider in the OOS window than in the IS region, the derived stop is too tight for the market it is used in, every label was cut on the wrong geometry, and the deploy gate certified a game the trade is not playing - the 2026-08-09 geometry mismatch arriving through drift rather than through a config error. Reported in TWO currencies deliberately. A rank-test p says whether the distributions differ; it does not say whether anyone should care. The stop each half's own quantile would derive says exactly that, in ATR multiples - the units the order is placed in. A significant p with both stops on the same ladder rung is a curiosity; half an ATR of movement is a problem whether or not it clears 0.05. Declustered first, same as the regime test: overlapping labels are not independent draws. Harvest guards are copied from the deriver's own so the two describe the same sample - and the split is verified identical (totalIter == bars - historyBars, so Train's oosCutoff and the deriver's are the same number). Runs before the two model fits and needs only the excursion caches: a chart too thin to fit a forest can still have drifted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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);
//--- ...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;
//--- 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);
//--- 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
//--- 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.
fix(baselines): the budget said 45 s and the log said 548.8 s, because a boundary check cannot stop a phase in flight Yesterday's fix predicted the cross-validation's cost and declined it before starting, then left the linear fit to a plain boundary check. The 17:01 run says exactly what that was worth: ALGLIB MLP trained in 12.1 s ... NOTE: 6435 weights against 995 rows cross-validation SKIPPED - 3 folds x 12.1 s = ~36.3 s predicted against 31.9 s left Alglib baselines stopping before the OOS scoring - 548.8 s spent of a 45 s budget Everything up to the linear fit obeyed the budget. LRBuild then held the thread for ~535 s in one uninterruptible call and the check fired afterwards, on a decision that was already made. The same block shows up in the training timer as 'SLOW ERA heartbeat - net fwd/back 3.9s | everything else 566.2s', and in three members re-arming a study event that never arrived. LRBuild solves a (width+1)^2 normal-equation system: the cost grows as width^3 and ignores the row count entirely, so the row cap that fixed the forest and the MLP does nothing here. Predicted from that measurement and declined before it starts, like the CV. Second, independent reason to decline it: at 800 columns against 1000 rows the normal equations are singular, so any coefficients returned are one arbitrary solution of infinitely many. That fit would have been printed as a baseline while carrying no information. Both grounds are checked, and the log says which one applied. The OOS scoring - the phase that answers the question the suite exists for - has now failed to run twice for two different reasons. It is the first thing to check on the next attempt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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;
fix(baselines): the budget said 45 s and the log said 548.8 s, because a boundary check cannot stop a phase in flight Yesterday's fix predicted the cross-validation's cost and declined it before starting, then left the linear fit to a plain boundary check. The 17:01 run says exactly what that was worth: ALGLIB MLP trained in 12.1 s ... NOTE: 6435 weights against 995 rows cross-validation SKIPPED - 3 folds x 12.1 s = ~36.3 s predicted against 31.9 s left Alglib baselines stopping before the OOS scoring - 548.8 s spent of a 45 s budget Everything up to the linear fit obeyed the budget. LRBuild then held the thread for ~535 s in one uninterruptible call and the check fired afterwards, on a decision that was already made. The same block shows up in the training timer as 'SLOW ERA heartbeat - net fwd/back 3.9s | everything else 566.2s', and in three members re-arming a study event that never arrived. LRBuild solves a (width+1)^2 normal-equation system: the cost grows as width^3 and ignores the row count entirely, so the row cap that fixed the forest and the MLP does nothing here. Predicted from that measurement and declined before it starts, like the CV. Second, independent reason to decline it: at 800 columns against 1000 rows the normal equations are singular, so any coefficients returned are one arbitrary solution of infinitely many. That fit would have been printed as a baseline while carrying no information. Both grounds are checked, and the log says which one applied. The OOS scoring - the phase that answers the question the suite exists for - has now failed to run twice for two different reasons. It is the first thing to check on the next attempt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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"
fix(baselines): the budget said 45 s and the log said 548.8 s, because a boundary check cannot stop a phase in flight Yesterday's fix predicted the cross-validation's cost and declined it before starting, then left the linear fit to a plain boundary check. The 17:01 run says exactly what that was worth: ALGLIB MLP trained in 12.1 s ... NOTE: 6435 weights against 995 rows cross-validation SKIPPED - 3 folds x 12.1 s = ~36.3 s predicted against 31.9 s left Alglib baselines stopping before the OOS scoring - 548.8 s spent of a 45 s budget Everything up to the linear fit obeyed the budget. LRBuild then held the thread for ~535 s in one uninterruptible call and the check fired afterwards, on a decision that was already made. The same block shows up in the training timer as 'SLOW ERA heartbeat - net fwd/back 3.9s | everything else 566.2s', and in three members re-arming a study event that never arrived. LRBuild solves a (width+1)^2 normal-equation system: the cost grows as width^3 and ignores the row count entirely, so the row cap that fixed the forest and the MLP does nothing here. Predicted from that measurement and declined before it starts, like the CV. Second, independent reason to decline it: at 800 columns against 1000 rows the normal equations are singular, so any coefficients returned are one arbitrary solution of infinitely many. That fit would have been printed as a baseline while carrying no information. Both grounds are checked, and the log says which one applied. The OOS scoring - the phase that answers the question the suite exists for - has now failed to run twice for two different reasons. It is the first thing to check on the next attempt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.",
fix(baselines): the budget said 45 s and the log said 548.8 s, because a boundary check cannot stop a phase in flight Yesterday's fix predicted the cross-validation's cost and declined it before starting, then left the linear fit to a plain boundary check. The 17:01 run says exactly what that was worth: ALGLIB MLP trained in 12.1 s ... NOTE: 6435 weights against 995 rows cross-validation SKIPPED - 3 folds x 12.1 s = ~36.3 s predicted against 31.9 s left Alglib baselines stopping before the OOS scoring - 548.8 s spent of a 45 s budget Everything up to the linear fit obeyed the budget. LRBuild then held the thread for ~535 s in one uninterruptible call and the check fired afterwards, on a decision that was already made. The same block shows up in the training timer as 'SLOW ERA heartbeat - net fwd/back 3.9s | everything else 566.2s', and in three members re-arming a study event that never arrived. LRBuild solves a (width+1)^2 normal-equation system: the cost grows as width^3 and ignores the row count entirely, so the row cap that fixed the forest and the MLP does nothing here. Predicted from that measurement and declined before it starts, like the CV. Second, independent reason to decline it: at 800 columns against 1000 rows the normal equations are singular, so any coefficients returned are one arbitrary solution of infinitely many. That fit would have been printed as a baseline while carrying no information. Both grounds are checked, and the log says which one applied. The OOS scoring - the phase that answers the question the suite exists for - has now failed to run twice for two different reasons. It is the first thing to check on the next attempt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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
//--- 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();
}
//+------------------------------------------------------------------+
//| 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;
}
//+------------------------------------------------------------------+
//| 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
}
//+------------------------------------------------------------------+
//| 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
}
//+------------------------------------------------------------------+
//| 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
}
//+------------------------------------------------------------------+
//| 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;
//--- 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);
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"
" %.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")));
}
feat(baselines): geometry-drift check on the derived stop DeriveBarrierGeometry() reads the stop off a quantile of the adverse excursions in the IS region ONLY - correctly, since a geometry chosen with the holdout in view has used the holdout for selection and it stops being a holdout. The cost of that correct choice is that nothing ever checked whether the distribution it measured still holds on the bars the model actually trades. If adverse excursions run wider in the OOS window than in the IS region, the derived stop is too tight for the market it is used in, every label was cut on the wrong geometry, and the deploy gate certified a game the trade is not playing - the 2026-08-09 geometry mismatch arriving through drift rather than through a config error. Reported in TWO currencies deliberately. A rank-test p says whether the distributions differ; it does not say whether anyone should care. The stop each half's own quantile would derive says exactly that, in ATR multiples - the units the order is placed in. A significant p with both stops on the same ladder rung is a curiosity; half an ATR of movement is a problem whether or not it clears 0.05. Declustered first, same as the regime test: overlapping labels are not independent draws. Harvest guards are copied from the deriver's own so the two describe the same sample - and the split is verified identical (totalIter == bars - historyBars, so Train's oosCutoff and the deriver's are the same number). Runs before the two model fits and needs only the excursion caches: a chart too thin to fit a forest can still have drifted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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