2026-08-01 11:27:28 -04:00
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Lifecycle.mqh |
|
|
|
|
|
//| |
|
|
|
|
|
//| Construction/destruction, the CExpertSignal vote API |
|
|
|
|
|
//| (LongCondition/ShortCondition/ConfidenceTier/pattern weights), |
|
|
|
|
|
//| tick + chart-event dispatch, and the per-config chart lock. |
|
|
|
|
|
//| |
|
|
|
|
|
//| PARTIAL IMPLEMENTATION FILE - not standalone. |
|
|
|
|
|
//| CExpertSignalAIBase method BODIES only. The class declaration |
|
|
|
|
|
//| lives in Expert\ExpertSignalAIBase.mqh, which includes this file |
|
|
|
|
|
//| at the bottom, after the declaration. Do not include it |
|
|
|
|
|
//| anywhere else and do not compile it on its own. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
#ifndef WARRIOR_AIBASE_LIFECYCLE_MQH
|
|
|
|
|
#define WARRIOR_AIBASE_LIFECYCLE_MQH
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Constructor |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//--- These are only fallback defaults for a fresh object before the EA's OnInit() applies the
|
|
|
|
|
//--- active input values via the public setters in Warrior_EA.mq5. The input-driven values are the
|
|
|
|
|
//--- source of truth for the actual run configuration.
|
|
|
|
|
CExpertSignalAIBase::CExpertSignalAIBase(void) :
|
|
|
|
|
ID("NULL"),
|
|
|
|
|
m_neuronsCount(0),
|
|
|
|
|
m_minTrainYear(1970),
|
|
|
|
|
m_optimizationAlgo(TrainingOptimizer), // see the member declaration comment
|
|
|
|
|
//--- Placeholder only; InitNeuralNetwork() replaces it with ComputeFirstLayerWidth() before anything
|
|
|
|
|
//--- reads it. Deliberately the floor rather than 0, so a hypothetical path that built a topology
|
|
|
|
|
//--- without going through init would produce a small usable net instead of a zero-width layer.
|
|
|
|
|
m_initialNeuronsCount(FIRST_LAYER_MIN_WIDTH),
|
|
|
|
|
m_outputNeuronsCount(OUTPUT_CLASSIFICATION),
|
|
|
|
|
//--- Frozen. Nothing reads these to build a topology any more - the taper derives its own endpoints
|
|
|
|
|
//--- (BuildFreshTopology) - but they still occupy positional slots in the .cfg sidecar and the weights
|
|
|
|
|
//--- fingerprint. Held at their historical defaults so both stay byte-stable; changing either value
|
|
|
|
|
//--- would re-key every model on disk for no behavioural reason whatsoever.
|
|
|
|
|
m_minNeuronsCount(MIN_NEURONS_20),
|
|
|
|
|
m_neuronsReduction(RF_70),
|
|
|
|
|
m_hiddenLayersCount(3),
|
|
|
|
|
m_lstmHiddenSize(32),
|
|
|
|
|
m_convFilterCount(16),
|
|
|
|
|
m_historyBars(14),
|
|
|
|
|
m_fractalPeriods(5),
|
|
|
|
|
m_pattern_0(25),
|
|
|
|
|
m_pattern_1(50),
|
|
|
|
|
m_pattern_2(75),
|
|
|
|
|
m_pattern_3(100),
|
|
|
|
|
m_useVolumes(true),
|
|
|
|
|
m_useTime(true),
|
|
|
|
|
m_useATR(true),
|
|
|
|
|
m_useMA(false),
|
|
|
|
|
m_useRSI(false),
|
|
|
|
|
m_useMACD(false),
|
|
|
|
|
m_useIchimoku(false),
|
|
|
|
|
m_useSwingContext(false),
|
|
|
|
|
m_useNews(false),
|
fix(signals): revive a dead MA model, and demote Sanyaku from state to event
Two defects surfaced by research/test_classic.py, both verified fixed by re-running the
transcription against 178k bars of EURUSD H1.
CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the
shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so
DiffMA(i) = a * (Close(i) - MA(i+1))
DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1))
are positive multiples of one quantity and always share a sign. Model 1 asks for a close
BELOW a RISING average, which is precisely the combination that identity forbids: 0.000%
of bars, either direction, any symbol. The MQL5 standard library this was ported from
defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA
default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for
every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars.
CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing
STATES conjoined with no transition term, so it held across long stretches - and being
last in the if-chain at the top weight, the module's highest-conviction reading was also
its most common one, overwriting all eight event models below it on a quarter of all bars.
The old comment rejected an event form because "demanding all three flip on the same bar
would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the
ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1)
fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the
strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback.
Neither pattern showed edge before or after; this is about the models meaning what they
say and the vote not being dominated by a constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
|
|
|
m_useCrossAsset(false),
|
feat(ai): spread as a volatility-regime feature, and fix a stale-index cache in both new blocks
Adds spread/ATR and the spread change ratio as network inputs (EnableSpreadFeature,
default on). Spread is the one microstructure channel that is both FX-available and
genuinely historical in the Strategy Tester - "during testing, the spread is not modeled
but is taken from historical data" - so unlike swap, signed tick flow or depth of market it
is something a backtest can honestly validate.
What it encodes, stated precisely because the raw measurement overstates it.
research/test_spread.py found spr/atr the strongest single feature in this codebase, on 5
of 8 instrument/geometry cells at 2-4x any volume feature. But the barrier LABEL charges
the spread inside its own barriers, so a wide-spread bar is mechanically likelier to
resolve as a loss and the feature would partly be predicting its own cost model. Relabelling
at zero cost and re-measuring the identical feature showed 20-40% of it WAS that tautology
and the majority was not (XAUUSD retained 97%). What survives is a volatility-regime
reading: spread is near-fixed while ATR is not, so the ratio runs high exactly when
realised volatility is below its own ATR estimate, which genuinely predicts whether
ATR-scaled barriers get reached. It is UNSIGNED - Neutral-vs-directional only, never a side.
Also fixes a stale-index bug I introduced with the cross-asset panel and had just repeated
in the spread series. Both cached on length alone:
if(m_crossAsset.Bars() >= bars) return true;
MQL5 series indices are relative to NOW, so one new closed candle shifts every index by
one. Keyed only on length, the panel keeps serving its index 0 as a bar that is no longer
the newest, and every cross-asset value is read one bar out of step with the price features
sitting beside it in the same vector - silently, with no error and no shape change. This is
the same class of defect as the dtStudied watermark behind the zero-direction backtests.
Both now carry a datetime anchor on m_Time.GetData(0), the same invalidation key the
label/feature bar caches already use.
And a performance fix that fell out of it: with correct invalidation the panel rebuilds on
every new bar, and RefreshConvergedSignal runs per bar - which in the tester would mean one
full multi-symbol resample per simulated bar at training depth. Inference only reads bars
0..m_historyBars-1 plus the panel's own slow window, so it now requests exactly that. The
cache check is >=, so a deeper panel left from training still satisfies it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:42:40 -04:00
|
|
|
m_useSpreadFeature(false),
|
|
|
|
|
m_spreadSeriesBars(0),
|
|
|
|
|
m_spreadSeriesAnchor(0),
|
|
|
|
|
m_crossAssetAnchor(0),
|
2026-08-01 11:27:28 -04:00
|
|
|
m_newsFeatureWindowMinutes(60),
|
|
|
|
|
m_useADCumulativeDelta(false),
|
|
|
|
|
m_useADShorteningOfThrust(false),
|
|
|
|
|
m_useADWyckoffEventStream(false),
|
|
|
|
|
m_useADWyckoffFailedStructure(false),
|
|
|
|
|
m_useADWyckoffSignificantBarInversion(false),
|
|
|
|
|
m_autoTuneIndicators(false),
|
|
|
|
|
m_indicatorsPtr(NULL),
|
|
|
|
|
Net(NULL),
|
|
|
|
|
m_shadowNet(NULL),
|
|
|
|
|
TempData(NULL),
|
|
|
|
|
dError(-1),
|
|
|
|
|
dUndefine(0),
|
|
|
|
|
dForecast(0),
|
|
|
|
|
dPrevSignal(0),
|
|
|
|
|
m_refreshOk(0),
|
|
|
|
|
m_refreshFailFeatures(0),
|
|
|
|
|
m_refreshFailShort(0),
|
|
|
|
|
m_refreshBuy(0),
|
|
|
|
|
m_refreshSell(0),
|
|
|
|
|
m_refreshNeutral(0),
|
|
|
|
|
m_voteGateBlocked(0),
|
|
|
|
|
m_voteGatePassed(0),
|
|
|
|
|
m_voteGateCompleteAtFirst(-1),
|
|
|
|
|
m_voteGateLoadedAtFirst(-1),
|
|
|
|
|
m_signalClusterWindow(6),
|
|
|
|
|
m_nmsLiveBuyTime(0),
|
|
|
|
|
m_nmsLiveSellTime(0),
|
|
|
|
|
m_nmsLiveBuyAccept(false),
|
|
|
|
|
m_nmsLiveSellAccept(false),
|
|
|
|
|
m_nmsLiveKeptTime(0),
|
|
|
|
|
m_nmsLiveKeptDir(Neutral),
|
|
|
|
|
m_nmsLiveKeptConf(0),
|
|
|
|
|
dtStudied(0),
|
|
|
|
|
m_eraCount(0),
|
|
|
|
|
m_trainingComplete(false),
|
|
|
|
|
m_inferenceOnly(false),
|
|
|
|
|
m_modelLoadedFromDisk(false),
|
|
|
|
|
m_topologySuperseded(false),
|
|
|
|
|
m_mqlInferenceValidated(false),
|
|
|
|
|
m_shadowBootstrapAttempted(false),
|
|
|
|
|
m_enableOnlineLearning(true),
|
|
|
|
|
m_freezePriorCalibration(false),
|
|
|
|
|
m_onlineLearnedUpToTime(0),
|
|
|
|
|
m_onlineRollingAcc(-1.0),
|
|
|
|
|
m_onlineSamples(0),
|
|
|
|
|
m_onlineBarsSincePersist(0),
|
|
|
|
|
m_onlineBlendFrozen(false),
|
|
|
|
|
bEventStudy(false),
|
|
|
|
|
m_oosSplitPct(30),
|
|
|
|
|
dOosError(-1),
|
|
|
|
|
dOosForecast(0),
|
|
|
|
|
m_oosSamples(0),
|
|
|
|
|
m_cumIsCorrect(0),
|
|
|
|
|
m_cumIsTotal(0),
|
|
|
|
|
m_cumOosCorrect(0),
|
|
|
|
|
m_cumOosTotal(0),
|
|
|
|
|
m_oosOutSpreadSum(0),
|
|
|
|
|
m_oosOutCount(0),
|
|
|
|
|
m_countBuySignals(0),
|
|
|
|
|
m_countSellSignals(0),
|
|
|
|
|
m_countNeutralSignals(0),
|
|
|
|
|
m_trueBuyCount(0),
|
|
|
|
|
m_trueSellCount(0),
|
|
|
|
|
m_trueNeutralCount(0),
|
|
|
|
|
m_logitAdjustTau(1.0),
|
|
|
|
|
m_logitAdjustLogged(false),
|
|
|
|
|
m_logitAdjustSkipWarned(false),
|
|
|
|
|
m_prevEraTrueBuyCount(0),
|
|
|
|
|
m_prevEraTrueSellCount(0),
|
|
|
|
|
m_prevEraTrueNeutralCount(0),
|
|
|
|
|
m_oosBuyHits(0),
|
|
|
|
|
m_oosBuyTotal(0),
|
|
|
|
|
m_oosSellHits(0),
|
|
|
|
|
m_oosSellTotal(0),
|
|
|
|
|
m_oosNeutralHits(0),
|
|
|
|
|
m_oosNeutralTotal(0),
|
|
|
|
|
m_oosBuyPredicted(0),
|
|
|
|
|
m_oosBuyPredictedHits(0),
|
|
|
|
|
m_oosSellPredicted(0),
|
|
|
|
|
m_oosSellPredictedHits(0),
|
|
|
|
|
m_oosNeutralPredicted(0),
|
|
|
|
|
m_oosNeutralPredictedHits(0),
|
|
|
|
|
m_oosConfidenceSum(0),
|
|
|
|
|
m_confidenceCalScale(1.0),
|
|
|
|
|
m_minDirectionalRecallPct(40),
|
|
|
|
|
m_priorBuy(0.0),
|
|
|
|
|
m_priorSell(0.0),
|
|
|
|
|
m_priorNeutral(0.0),
|
|
|
|
|
m_oosBuyFired(0),
|
|
|
|
|
m_oosBuyFiredHits(0),
|
|
|
|
|
m_oosSellFired(0),
|
|
|
|
|
m_oosSellFiredHits(0),
|
|
|
|
|
m_lastBuyFiredPrecPct(-1),
|
|
|
|
|
m_lastSellFiredPrecPct(-1),
|
|
|
|
|
m_lastBuyFired(0),
|
|
|
|
|
m_lastSellFired(0),
|
|
|
|
|
m_maxClassSampleWeight(1.5),
|
|
|
|
|
m_swingConfirmationBars(100),
|
|
|
|
|
m_barrierHorizonBars(BARRIER_HORIZON_FALLBACK),
|
|
|
|
|
m_barrierHorizonResolved(false),
|
|
|
|
|
m_barrierFallbackWarned(false),
|
|
|
|
|
m_lastBarrierTimedOut(false),
|
|
|
|
|
m_labelPrebuildTimeoutCount(0),
|
|
|
|
|
m_maxErasPerRun(300),
|
|
|
|
|
m_arrowRestoreIndex(0),
|
|
|
|
|
m_arrowRestorePending(false),
|
|
|
|
|
m_arrowRestoreStartMs(0),
|
|
|
|
|
m_rescanIndex(0),
|
|
|
|
|
m_rescanHi(0),
|
|
|
|
|
m_rescanBarsNow(0),
|
|
|
|
|
m_rescanPending(false),
|
|
|
|
|
m_rescanStartMs(0),
|
|
|
|
|
m_rescanRawBuy(0),
|
|
|
|
|
m_rescanRawSell(0),
|
|
|
|
|
m_rescanRawNeutral(0),
|
|
|
|
|
m_trainRunActive(false),
|
|
|
|
|
m_eraResumePending(false),
|
|
|
|
|
m_resumeBars(0),
|
|
|
|
|
m_resumeTotalIter(0),
|
|
|
|
|
m_resumeOosCutoff(0),
|
|
|
|
|
m_resumeBarIndex(0),
|
|
|
|
|
m_resumeAddLoop(false),
|
|
|
|
|
m_isTrainQueueCount(0),
|
|
|
|
|
m_isTrainCursor(0),
|
|
|
|
|
m_isPass2Active(false),
|
|
|
|
|
m_isPass2Done(false),
|
|
|
|
|
m_isPass3Active(false),
|
|
|
|
|
m_oosScoreIndex(0),
|
|
|
|
|
m_oosScoreStartIndex(0),
|
|
|
|
|
m_lastStatusLabelUpdateTick(0),
|
|
|
|
|
m_lastBuyRecallPct(-1),
|
|
|
|
|
m_lastSellRecallPct(-1),
|
|
|
|
|
m_lastDisplayNeuron0(0),
|
|
|
|
|
m_lastDisplayNeuron1(0),
|
|
|
|
|
m_lastDisplayNeuron2(0),
|
|
|
|
|
m_lastDisplaySignal(0),
|
|
|
|
|
m_lastBarTime(0),
|
|
|
|
|
m_modelEta(InitialEtaForOptimizer()),
|
|
|
|
|
m_etaCeiling(InitialEtaForOptimizer()),
|
|
|
|
|
m_erasSinceCooldown(0),
|
|
|
|
|
m_bestOosForecast(-1),
|
|
|
|
|
m_bestBalancedOos(-1),
|
|
|
|
|
m_bestPassedRecall(false),
|
|
|
|
|
m_haveOosCheckpoint(false),
|
|
|
|
|
m_oosStable(false),
|
|
|
|
|
m_objectiveMet(false),
|
|
|
|
|
m_erasSinceBestBalanced(0),
|
|
|
|
|
m_plateauStage(0),
|
|
|
|
|
m_syncWaitStartTick(0),
|
|
|
|
|
m_warmupPassesRemaining(0),
|
|
|
|
|
m_labelCacheBars(0),
|
|
|
|
|
m_labelCacheAnchorTime(0),
|
|
|
|
|
m_labelCachePrebuilt(false),
|
feat: derive the ATR multiples from measured excursions - no hardcoded geometry
The barrier was still two constants. SL_Mode/TP_Mode left the Inputs tab in
3482b6c, but the fallback was a hardcoded 2:6 and the geometry scan only ever
chose from a hardcoded grid {2,3} x {2,3,4,6,8,10}. Picking the least-bad of
eleven guesses is not deriving anything.
WHY THE SCAN WAS THE WRONG INSTRUMENT, now measurable rather than argued. It
ranks pairings by how predictable their OUTCOME is - a question about direction.
The excursion test (2c78f3b) ran on SP500 H1 and direction is the one thing
absent: ASYMMETRY p=0.0846, against RANGE/UP/DOWN all at p=0.0050, with RANGE
scoring 0.01345 vs a 0.00343 null - 4x, where the barrier label sits at 1.01x.
Hence the scan failing its own gate on every run, and its "winner" wandering
2:8 -> 3:8 -> 2:8 -> 2:4 across four runs of the same data. Excursion SIZE is
strongly measurable, so derive the geometry from that instead.
stop = q25 of measured ADVERSE travel (ordinary noise does not reach it)
target = q50 of measured FAVOURABLE travel (reached ~half the time, by
construction, inside the horizon)
Continuous, in ATR units, superseding the enum multiples. Reachability ("target
on X% of bars, stop on Y%") and the implied break-even are printed so the choice
is auditable rather than trusted.
FIXED-POINT ITERATION, not one-shot. ComputeBarrierHorizonBars scales the
horizon with the target (first-passage time grows with the band) and the
excursions are measured OVER the horizon, so target -> horizon -> excursions ->
target is a real loop - deriving once sizes the target from travel measured
under the PREVIOUS horizon. Re-measures until the multiples move <5%, capped at
3 passes, and says so if it does not settle.
Does NOT create expectancy, and the log says as much: chance precision equals
break-even at every geometry (m/(m+k) on both sides). It buys a target the
market reaches and a stop that survives noise. Where Min_Risk_Reward_Ratio
forces a target the market rarely reaches, it WARNS rather than overriding -
the ratio is the user's risk policy, so the honest move is to state its cost.
That is the collision that once rejected 100% of setups.
Pinned in the .cfg as doubles appended AFTER this morning's two ints, so .cfg
files written earlier today still load (their length guard finds no doubles) and
a model that carries them was trained on them and never re-derives.
Also fixes a message from e5ceed6 that claimed "this model resumed from disk"
unconditionally - it printed above a "seeding era 0" line on a brand-new model,
because the branch fires whenever the cache is not built, which is equally true
before a fresh model's first prebuild. A diagnostic that misreports its own
trigger is worse than one that says nothing: it gets quoted back as evidence.
FORCES A FULL RETRAIN (labels change).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 12:06:25 -04:00
|
|
|
m_lastExcUp(0.0),
|
|
|
|
|
m_lastExcDown(0.0),
|
|
|
|
|
m_derivedSlMult(0.0),
|
|
|
|
|
m_derivedTpMult(0.0),
|
|
|
|
|
m_geometryDerived(false),
|
|
|
|
|
m_geometryDerivePasses(0),
|
fix: excursion window must not depend on the barrier it sizes
DIRECTION IS NOT THERE, and this run is what establishes it. Three symbols:
raw ASYMMETRY clears on all three (p=0.0199 / 0.0050 / 0.0050)
norm ASYMMETRY collapses on all three (p=0.3433 / 0.5075 / 0.2736),
USDCAD landing BELOW its own null
RANGE control strengthens to 3-5x its null everywhere
Divide sigma out and the apparent directional signal vanishes entirely. What
cleared was volatility leaking through an unnormalised difference. Note this
would have passed any replication test: three instruments at p=0.005 is exactly
the evidence one would accept before committing to a rebuild, and the confound
reproduces perfectly. Replication was never going to catch it - only the
normalisation could.
Two defects of mine, both surfaced by the same run.
1. THE GEOMETRY DERIVATION WAS DIVERGING, NOT CONVERGING. It produced a
14.57*ATR stop and a 29.14*ATR target that only 5.7% of bars ever reach.
Excursions were measured over the barrier horizon; the horizon scales with
the target; the target is a quantile of the excursions - so target ->
horizon -> excursions -> target ran away, and "settled" only because the
horizon ladder caps at 384 bars. A saturated runaway, which the iteration
guard could not catch because it watches for OSCILLATION.
Fixed at the root: excursions now accumulate only over m_swingMedianBars -
the UNSCALED median ZigZag leg, a property of the instrument that owes
nothing to the barrier. The barrier walk still runs the full horizon,
because that is how long the trade is held; only the MEASUREMENT used to
size the barrier is confined to a geometry-independent window.
(The Min_Risk_Reward_Ratio warning fired correctly and is what flagged it -
the diagnostic worked while the derivation behind it did not.)
2. THE CONFOUND VERDICT WAS UNREACHABLE. `sizeCleared && !asymCleared` was
tested first and is true whenever size clears - i.e. always - so the branch
that NAMES the volatility confound never printed; all three symbols showed
the generic size-not-direction message instead. Verdict chain rewritten with
the specific case first, and the dangling elses my first patch introduced
removed.
FORCES A FULL RETRAIN (the excursion window changes every derived barrier).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 13:57:23 -04:00
|
|
|
m_swingMedianBars(0),
|
2026-08-01 11:27:28 -04:00
|
|
|
m_labelPrebuildActive(false),
|
|
|
|
|
m_prebuildSeedPending(false),
|
|
|
|
|
m_labelPrebuildBars(0),
|
|
|
|
|
m_labelPrebuildOosCutoff(0),
|
|
|
|
|
m_labelPrebuildIndex(-1),
|
|
|
|
|
m_labelPrebuildBuyCount(0),
|
|
|
|
|
m_labelPrebuildSellCount(0),
|
|
|
|
|
m_labelPrebuildNeutralCount(0),
|
|
|
|
|
m_simOosNet(NULL),
|
|
|
|
|
m_simOosRunActive(false),
|
|
|
|
|
m_simOosCutoff(0),
|
|
|
|
|
m_simOosBarIndex(-1),
|
|
|
|
|
m_simOosForecast(0),
|
|
|
|
|
m_simOosSamples(0),
|
|
|
|
|
m_tuneTrialIndex(-1),
|
|
|
|
|
m_tuneBestOosForecast(-1),
|
|
|
|
|
m_tuneLastTrialWasWin(true),
|
|
|
|
|
m_tuneHaveBestCheckpoint(false),
|
|
|
|
|
m_tuneStartTrainBar(0),
|
|
|
|
|
m_tuneFilterDone(false),
|
|
|
|
|
m_trainingPaused(false),
|
|
|
|
|
m_trainingStopRequested(false),
|
|
|
|
|
m_activeFileCommon(true),
|
|
|
|
|
m_configLockName(""),
|
|
|
|
|
m_isInitialized(false),
|
fix(deinit): a full model write was running ahead of the cheap cleanup
"Abnormal termination" is back, and this time it is not the arrows. The
timing names the culprit exactly:
16:02:31.547 OnDeinit: shutting down
16:02:36.003 Abnormal termination <- 4.46 s, MetaTrader gave up
16:02:36.226 chart signals - persisted <- cleanup finished 0.2 s LATE
OnDeinit called StopTraining() BEFORE the chart cleanup. StopTraining()
finalises an in-flight run, and FinalizeTrainRun() restores the best
checkpoint and then persists it - a full ~1MB model write per signal. So
the expensive step ran ahead of the cheap bounded one, which is precisely
the inversion the shutdown ordering exists to prevent. The previous fix
put PersistWeightsOnShutdown last and missed that StopTraining smuggles a
second save in at the front.
Two changes:
Cleanup now runs FIRST, then StopTraining, then the weight save. The
visible teardown is cheap and bounded, so it always completes even when
everything after it is killed.
And the deploy-persist inside FinalizeTrainRun is suppressed during
shutdown. RestoreWeights() is an in-MEMORY swap, so the best checkpoint
is already the live net by that line, and PersistWeightsOnShutdown writes
exactly those weights moments later. The old path wrote the same model
twice per signal - eight full writes across four charts - for no benefit.
A user-pressed Stop still persists immediately, because nothing else
would.
Compiles 0 errors / 0 warnings. Build tag deinit-order-v2.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 16:06:40 -04:00
|
|
|
m_shutdownInProgress(false),
|
diag(autotune): one label shuffle cannot settle the no-edge question
The permutation baseline added in 018afb1 came back on all four charts as
0.00401 nats against floors of 0.00267 / 0.00298 / 0.00318 - three draws
whose spread is as wide as the excess being judged, because one shuffle
is one sample from the null, not the null. That is not enough to retire a
topology on.
Now MI_NOISE_PERMUTATIONS draws, reported as mean +/- sd with a z-score,
plus two numbers the mean over 26 columns cannot express:
- the STRONGEST single feature's MI, against its own shuffled value.
One informative column among 25 useless ones is precisely the case
the mean hides, and precisely the case worth finding.
- the excess as a percentage of H(Y). At these sample sizes a z-score
can be comfortably significant while the effect is worthless, so
"is it real" and "is it big enough to matter" are asked separately
and answered separately.
The verdict line also now states the measure's limit every time rather
than only when the news is bad: this is a MARGINAL, PER-BAR statistic and
the network reads m_historyBars bars jointly, so it can prove signal
exists but never that it does not. It rules out a per-feature edge - and
therefore any indicator retuning - not an edge that lives in a
combination or across time.
Compiles 0 errors / 0 warnings, standard and Market.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 13:32:12 -04:00
|
|
|
m_lastArrowsSaved(0),
|
|
|
|
|
m_miBestColumn(0.0),
|
2026-08-01 14:01:32 -04:00
|
|
|
m_miLabelEntropy(0.0),
|
diag(autotune): a positive control, and a scan that separates "no signal"
from "signal knocked out of step"
Four architecturally different networks landed on the same precision -
Buy 23-25% against a 25.4% base rate, Sell 19-22% against 22.0% - while
making completely different calls (HYBRID votes Sell on 69% of bars, PAI
on 41%). Precision equal to the base rate is what INDEPENDENCE looks
like, and precision under independence is fixed by the label
distribution, not by the architecture, so all four converging on it is
arithmetic rather than coincidence. Accuracy meanwhile tracks coverage
exactly as independence predicts (31.1/30.3/25.0 predicted vs
31.8/28.9/24.6 observed for PAI/CONV/HYB).
But "no information in the data" and "information destroyed upstream of
every topology" produce that identical picture, and the MI test alone
cannot tell them apart either. Two additions:
POSITIVE CONTROL. Three "measurements" in this codebase have turned out
to be silent no-ops that produced plausible numbers - the MI scorer
reading an array nobody filled, the eval-mode guard that switched off the
imbalance correction, the alternation gate whose premise was never true.
So the estimator now has to prove it responds to a signal known to be
present before any floor reading is believed: the label of a neighbouring
sample row, ~19 bars away and far inside the 128-bar barrier horizon, so
the two outcome windows overlap heavily and MUST be associated. Same
binning, same estimator. Near the floor => every MI figure is void.
ALIGNMENT SCAN. Re-scores against the label taken from bar i+k for k in
-5..+5. A peak at k != 0 is a feature/label misalignment - an off-by-one
in the label index, a horizon applied to the wrong bar, a feature window
that lags what it claims - which would destroy the information before any
topology saw it and would look identical in every accuracy number this EA
prints. A flat profile says the features simply do not carry this target.
The sampled range is trimmed by |k| at both ends so a shift is measured
rather than an edge effect, and both bars must carry a real label.
Also: BuildMiSample publishes its stride instead of the report
recomputing that arithmetic (it would drift), and the control sizes its
buffers from its own sample count rather than the caller's.
Compiles 0 errors / 0 warnings, standard and Market.
Build tag mi-control-align-v1. Redeploy only - no retrain, no model
deletion; the diagnostic runs on resumed models.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:12:10 -04:00
|
|
|
m_miStrideBars(0),
|
fix(diag): the symbol sweep was measuring its own sampling, not the market
Twelve cells came back with higher-timeframe "signal" 5-9x anything on
H1, at p=0.005. It was an artifact, and the sweep's own columns gave it
away: excess tracked the sampling STRIDE almost monotonically, and the
three D1 cells - stride collapsed to 1-5 bars against a 128-bar horizon,
i.e. ~99% window overlap - were the three highest. Three flaws, all the
same family: comparing numbers without the spread that belongs to them.
1. THE NULL ASSUMED INDEPENDENCE THE LABELS DO NOT HAVE. Triple-barrier
labels overlap; two rows less than one horizon apart share most of their
outcome window. A free Fisher-Yates shuffle destroys that dependence
along with the association, making the null far narrower than the truth
and handing out significance that isn't there - Lopez de Prado ch. 4
arriving through the back door of the significance test. Now permutes
contiguous BLOCKS of at least one horizon, so the null keeps the
autocorrelation and the p-value means what it says. It degrades honestly:
severe overlap leaves few blocks, the null widens, nothing reaches
significance. The block count is now printed, because THAT - not the row
count - is the sample size a p-value rests on, and a warning fires under
30 blocks so "not significant" is not misread as "no signal" when it
means "not enough independent history to tell".
2. THE POSITIVE CONTROL'S STRENGTH DEPENDED ON THE DATASET. It paired
each row's label with the NEXT SAMPLE ROW's, whose distance is the
stride - so on M5, where stride ran 160-717 bars against a 128-bar
horizon, it was pairing two windows that never overlap. All three M5
cells duly reported a FAILED estimator and voided their own results with
nothing wrong. A control whose strength varies with the cell cannot
certify the cell. Now pinned to a quarter of the horizon, where ~75%
overlap is guaranteed by construction.
3. THE LOOKAHEAD VERDICT HAD NO MARGIN. It flagged 7 of 12 cells on gaps
of 0.00008-0.00040 nats against a measured null sd of ~0.00030 - noise,
every one. Now requires 3 sd, the same discipline the deploy floor
applies to precision.
Compiles 0 errors / 0 warnings, standard and Market. Build tag
blockperm-v1. Supersedes every number from the sweep.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 15:11:40 -04:00
|
|
|
m_miNullBlocks(0),
|
feat(labels): measure which barrier is predictable at entry, don't guess
The alignment scan settled the shape of the problem: 4.7x more is
knowable 5 bars into a 128-bar window than at the entry the model
actually trades. A 6xATR target reached over 128 bars is decided
overwhelmingly by what happens DURING the window, so whatever the entry
state knows is buried under 128 bars of later noise. That is a property
of the TARGET, and it is why four different architectures all landed on
precision exactly equal to the base rate - no topology can undo it.
So measure the target. For each SL/TP pairing a user can actually select,
relabel the same sampled bars and score how much the SAME features say
about THAT outcome at entry. Seconds, no training, no topology, and it
runs on the diagnostic path that already exists.
Ranked on excess over its OWN null as a share of its OWN H(Y), never on
raw nats: each geometry has a different class balance, hence a different
finite-sample bias and a different amount of information there to find,
so raw MI would rank the most BALANCED label rather than the most
PREDICTABLE one. The break-even win rate m/(m+k) is printed beside each
so the ranking is read next to the bar the model must clear.
Stated in the output because it is the easy thing to get wrong: chance
precision EQUALS break-even at every geometry, so a tighter target does
not hand you expectancy. It buys predictability - less noise piled on top
of what the entry state knows - which is the one thing changing topology
cannot do.
Read-only by construction: it relabels a sampled copy via
TripleBarrierLabel(), never writes the label cache (which belongs to the
configured geometry), and restores the horizon and overrides it borrowed.
The overrides apply only when BOTH are positive, so a half-set pair can
never silently relabel a live run.
Compiles 0 errors / 0 warnings, standard and Market. Build tag
geometry-scan-v1. Redeploy only - no retrain to READ the ranking.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:32:04 -04:00
|
|
|
m_miReportDone(false),
|
2026-08-02 12:25:20 -04:00
|
|
|
m_miReportDeferrals(0),
|
feat(labels): measure which barrier is predictable at entry, don't guess
The alignment scan settled the shape of the problem: 4.7x more is
knowable 5 bars into a 128-bar window than at the entry the model
actually trades. A 6xATR target reached over 128 bars is decided
overwhelmingly by what happens DURING the window, so whatever the entry
state knows is buried under 128 bars of later noise. That is a property
of the TARGET, and it is why four different architectures all landed on
precision exactly equal to the base rate - no topology can undo it.
So measure the target. For each SL/TP pairing a user can actually select,
relabel the same sampled bars and score how much the SAME features say
about THAT outcome at entry. Seconds, no training, no topology, and it
runs on the diagnostic path that already exists.
Ranked on excess over its OWN null as a share of its OWN H(Y), never on
raw nats: each geometry has a different class balance, hence a different
finite-sample bias and a different amount of information there to find,
so raw MI would rank the most BALANCED label rather than the most
PREDICTABLE one. The break-even win rate m/(m+k) is printed beside each
so the ranking is read next to the bar the model must clear.
Stated in the output because it is the easy thing to get wrong: chance
precision EQUALS break-even at every geometry, so a tighter target does
not hand you expectancy. It buys predictability - less noise piled on top
of what the entry state knows - which is the one thing changing topology
cannot do.
Read-only by construction: it relabels a sampled copy via
TripleBarrierLabel(), never writes the label cache (which belongs to the
configured geometry), and restores the horizon and overrides it borrowed.
The overrides apply only when BOTH are positive, so a half-set pair can
never silently relabel a live run.
Compiles 0 errors / 0 warnings, standard and Market. Build tag
geometry-scan-v1. Redeploy only - no retrain to READ the ranking.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:32:04 -04:00
|
|
|
m_barrierScanSlMult(0.0),
|
|
|
|
|
m_barrierScanTpMult(0.0),
|
fix(labels): the geometry scan rewarded the labels it should reject
First run named 3:10 on all four charts, at 2.3x the configured 2:6. That
answer was wrong and the fault was the ranking statistic.
3:10 wants a horizon of ~swingMedian*30 (~320 bars) and gets
BARRIER_HORIZON_MAX. Clamped, most trades never resolve, the unresolved
remainder all lands in Neutral, and H(Y) collapses. The old statistic
divided the excess BY H(Y) - so a collapsing denominator made the most
degenerate label look like the most predictable one. Every geometry from
2:6 upward was already showing the clamped h128, and the two widest
scored highest, which is the fingerprint of the artefact rather than of
signal.
Two fixes:
Rank on the raw excess in nats. Subtracting each geometry's OWN measured
null already removes the class-balance bias, which is the only thing the
normalisation was ever needed for.
Disqualify clamped geometries outright rather than ranking them down. The
deployed EA holds until SL or TP with no bar limit, so a truncated label
trains the model on a question the strategy never asks. They are still
printed, marked '!', so the disqualification is visible instead of a
silent omission - and the scan now says so explicitly when nothing
eligible is left, because "the limit is the feature set, not the target"
is itself the finding in that case.
The scan also reports each geometry's directional share and timeout share
now. A label nobody can trade is not a candidate however well it scores,
and that has to be visible in the same line as the score.
Compiles 0 errors / 0 warnings. Build tag geometry-scan-v2.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:46:25 -04:00
|
|
|
m_barrierScanLiveLabels(false),
|
|
|
|
|
m_barrierScanTimeouts(0),
|
|
|
|
|
m_barrierHorizonClamped(false)
|
2026-08-01 11:27:28 -04:00
|
|
|
{
|
|
|
|
|
//--- indicator tuning defaults live in CADIndicatorTuner's own constructor (Expert\ADIndicatorTuner.mqh),
|
|
|
|
|
//--- which runs automatically for the m_indicatorTuner member above.
|
|
|
|
|
}
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Destructor |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
CExpertSignalAIBase::~CExpertSignalAIBase(void)
|
|
|
|
|
{
|
|
|
|
|
//--- deliberately NOT calling PersistOnShutdown() here: OnDeinit() (Warrior_EA.mq5) already calls
|
|
|
|
|
//--- it explicitly for every signal, one call stack frame shallower, BEFORE Expert.Deinit() tears
|
|
|
|
|
//--- these objects down. Doing it again here nested inside that same teardown cascade doubled the
|
|
|
|
|
//--- stack depth of an already-deep recursive save (layers -> neurons -> connections) right at the
|
|
|
|
|
//--- point in MT5's lifecycle (EA recompile while attached) that has the least stack headroom, and
|
|
|
|
|
//--- reliably crashed the terminal with a stack overflow. Keep this destructor cheap.
|
|
|
|
|
if(CheckPointer(Net) != POINTER_INVALID)
|
|
|
|
|
delete Net;
|
|
|
|
|
if(CheckPointer(m_shadowNet) != POINTER_INVALID)
|
|
|
|
|
delete m_shadowNet;
|
|
|
|
|
if(CheckPointer(TempData) != POINTER_INVALID)
|
|
|
|
|
delete TempData;
|
|
|
|
|
if(CheckPointer(m_simOosNet) != POINTER_INVALID)
|
|
|
|
|
delete m_simOosNet;
|
fix(chart): arrows survived the EA that drew them - persist, then clear
Reported: on deinit the panel and status label go, the signal arrows stay.
Two independent causes, both fixed here.
1. It was partly deliberate. ShutdownChartCleanup carried a second
behaviour selected by a `preserveChartArrows` flag derived from the
deinit reason: on RECOMPILE / PARAMETERS / CHARTCHANGE / TEMPLATE the
arrows were left on the chart on purpose, to avoid a reload flicker.
That branch IS the reported symptom, an operator cannot tell it apart
from a cleanup that failed, and it was outright wrong whenever the
reload changed the config - REASON_PARAMETERS means exactly that, and
the preserved arrows then belonged to a model the chart no longer
runs, with nothing marking them stale. It is gone, along with the flag
and m_purgeChartOnDestruct. One path now: persist, clear, restore on
the next attach.
2. Whatever remains was unfalsifiable. PurgeChart was a single
ObjectsDeleteAll(prefix) whose return value was discarded, with no
caller ever looking at the chart again - so "the arrows are still
there" and "the arrows were never there" produced identical evidence,
which is why the report survived three sessions. It now verifies:
after the bulk delete it walks the OBJ_ARROW-typed list (a handful of
objects, not the whole chart), deletes any surviving WarSig_ by name,
and says so. Costs one typed scan when the bulk delete works, which is
the normal case; names the root cause when it does not.
Every failure mode of SaveChartSignals was also silent - it returned void
and had three bare early returns. It returns bool now, logs the open
error with the filename, and the shutdown purge is CONDITIONAL on it: for
a converged model the chart objects are the only copy of its signal
history (nothing redraws them - the renderer runs per training era and a
deployed model has none left), so a chart left littered because the disk
write failed beats a clean chart bought by destroying the history. Either
way the log now says which happened.
Also states the user's rule once, where arrows come back rather than
across InitNeuralNetwork's several exits: no weights loaded for this
config => clear the sidecar and start visually clean. A fresh run must
not inherit calls it never made, and the first save would otherwise adopt
them (the sidecar is rebuilt by scanning the chart).
Compiles 0 errors / 0 warnings, standard and Market. Needs redeploy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 13:28:34 -04:00
|
|
|
//--- Unconditional now that the warm-reload "leave the arrows up" branch is gone (see
|
|
|
|
|
//--- ShutdownChartCleanup). Cheap and idempotent: OnDeinit already purged, so this normally deletes
|
|
|
|
|
//--- nothing - it exists for the teardown paths that never reach OnDeinit (a failed OnInit).
|
|
|
|
|
PurgeChart();
|
2026-08-01 11:27:28 -04:00
|
|
|
//--- Last, and cheap by design (one global-variable delete): the claim must outlive every save above
|
|
|
|
|
//--- it, or a chart re-attaching during this teardown could start writing the same files mid-save.
|
|
|
|
|
ReleaseConfigLock();
|
|
|
|
|
}
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Sets the file/id identity a subclass constructor would otherwise |
|
|
|
|
|
//| repeat verbatim (ID, m_id, m_folderPath, m_fileName, pattern count)|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
void CExpertSignalAIBase::SetIdentity(string id, string shortId, int patternCount = 4)
|
|
|
|
|
{
|
|
|
|
|
ID = id;
|
|
|
|
|
m_id = shortId;
|
|
|
|
|
m_folderPath = eaName + "\\" + "Neural Networks" + "\\" + "State" + "\\" + m_id + "\\";
|
|
|
|
|
m_fileName = m_folderPath + _Symbol + "_" + IntegerToString(_Period);
|
|
|
|
|
m_pattern_count = patternCount;
|
|
|
|
|
}
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| "Voting" that price will grow. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
int CExpertSignalAIBase::LongCondition(void)
|
|
|
|
|
{
|
|
|
|
|
int result = 0;
|
|
|
|
|
//--- Readiness gate: live trading still requires a converged model, but an inference-only tester run
|
|
|
|
|
//--- may replay a model that was ACTUALLY loaded from disk even if its persisted trainingComplete flag
|
|
|
|
|
//--- is false. Without that exception the tester could seed/refresh dPrevSignal from the deployed model
|
|
|
|
|
//--- and draw chart arrows from those weights, yet this gate would still hard-zero the trading vote.
|
|
|
|
|
//--- A fresh random topology still cannot trade in the tester because m_modelLoadedFromDisk stays false.
|
|
|
|
|
//--- Census: this gate is invisible to the refresh-path counters and is a live candidate for the
|
|
|
|
|
//--- all-bars-zero-direction backtest - see m_voteGateBlocked.
|
|
|
|
|
NoteVoteGate(DoubleToSignal(dPrevSignal) == Buy);
|
|
|
|
|
if(!m_trainingComplete && !(m_inferenceOnly && m_modelLoadedFromDisk))
|
|
|
|
|
return 0;
|
|
|
|
|
//--- No alternation gate any more - see the removal note at m_voteGateBlocked's declaration. Under
|
|
|
|
|
//--- triple-barrier labels consecutive same-direction setups are ordinary and correct.
|
|
|
|
|
//--- "not yet studied" sentinel - dPrevSignal == -2 is not a real Sell. Its MAGNITUDE is 2, so it
|
|
|
|
|
//--- passed straight through the confidence floor that used to sit here (|-2| exceeds any 0..1
|
|
|
|
|
//--- threshold); only the m_trainingComplete gate above was keeping it out. Checked explicitly now
|
|
|
|
|
//--- that the floor is gone, rather than left resting on that.
|
|
|
|
|
if(dPrevSignal == -2)
|
|
|
|
|
return 0;
|
|
|
|
|
//--- NO confidence floor here, by design - see m_minSignalConfidence's former declaration site. A
|
|
|
|
|
//--- weak call is not blocked at the AI's own boundary; it votes at its tier weight (as low as
|
|
|
|
|
//--- m_pattern_0) and is then filtered by Min vote to open, exactly like a weak classic vote.
|
|
|
|
|
if(DoubleToSignal(dPrevSignal) == Buy)
|
|
|
|
|
{
|
|
|
|
|
int tier = ConfidenceTier();
|
|
|
|
|
result = PatternWeightForTier(tier);
|
|
|
|
|
m_active_pattern = "Pattern_" + IntegerToString(tier);
|
|
|
|
|
m_active_direction = "Buy";
|
|
|
|
|
}
|
|
|
|
|
return(result);
|
|
|
|
|
}
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| "Voting" that price will fall. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
int CExpertSignalAIBase::ShortCondition(void)
|
|
|
|
|
{
|
|
|
|
|
int result = 0;
|
|
|
|
|
//--- Readiness gate - see LongCondition's matching comment.
|
|
|
|
|
NoteVoteGate(DoubleToSignal(dPrevSignal) == Sell);
|
|
|
|
|
if(!m_trainingComplete && !(m_inferenceOnly && m_modelLoadedFromDisk))
|
|
|
|
|
return 0;
|
|
|
|
|
//--- "not yet studied" sentinel, and no confidence floor - see LongCondition's matching comments.
|
|
|
|
|
if(dPrevSignal == -2)
|
|
|
|
|
return 0;
|
|
|
|
|
if(DoubleToSignal(dPrevSignal) == Sell)
|
|
|
|
|
{
|
|
|
|
|
int tier = ConfidenceTier();
|
|
|
|
|
result = PatternWeightForTier(tier);
|
|
|
|
|
m_active_pattern = "Pattern_" + IntegerToString(tier);
|
|
|
|
|
m_active_direction = "Sell";
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Buckets the live confidence magnitude into one of 4 equal bands |
|
|
|
|
|
//| between the head's own structural floor and 1.0 - see m_pattern_0's|
|
|
|
|
|
//| declaration comment for the resulting tier/weight table. Neither |
|
|
|
|
|
//| head has a configurable floor: the boundary is 1/3 for the 3-class|
|
|
|
|
|
//| softmax and 0.5 for regression (DoubleToSignal's own threshold), |
|
|
|
|
|
//| both arithmetic properties of the head rather than settings. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
int CExpertSignalAIBase::ConfidenceTier(void)
|
|
|
|
|
{
|
|
|
|
|
//--- The head's own STRUCTURAL decision boundary - the lowest confidence magnitude that head can
|
|
|
|
|
//--- possibly report for a directional call - not a user setting:
|
|
|
|
|
//--- - 3-class classification: the winning class of a 3-way softmax is arithmetically >= 1/3, since
|
|
|
|
|
//--- three probabilities summing to 1 cannot all be below it. Nothing can ever be read below this.
|
|
|
|
|
//--- - single-neuron regression: 0.5, DoubleToSignal()'s own decision boundary.
|
|
|
|
|
//--- Quartiling from HERE (rather than from an input, as the classification branch used to) is what
|
|
|
|
|
//--- makes the tier boundaries a fixed property of the model instead of something that silently moves
|
|
|
|
|
//--- whenever the trader adjusts an unrelated vote threshold - and it is what lets the same tier
|
|
|
|
|
//--- weights mean the same thing on both heads. See m_pattern_0's declaration comment for the weights.
|
|
|
|
|
double floorConf = (m_outputNeuronsCount == 3) ? (1.0 / 3.0) : 0.5;
|
|
|
|
|
double span = MathMax(1.0 - floorConf, 0.0001);
|
|
|
|
|
double t = (CalibratedConfidenceMagnitude() - floorConf) / span;
|
|
|
|
|
int tier = (int)MathFloor(t * 4.0);
|
|
|
|
|
return MathMax(0, MathMin(tier, 3));
|
|
|
|
|
}
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Returns the given tier's current pattern weight (0-100) |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
int CExpertSignalAIBase::PatternWeightForTier(int tier)
|
|
|
|
|
{
|
|
|
|
|
switch(tier)
|
|
|
|
|
{
|
|
|
|
|
case 0:
|
|
|
|
|
return m_pattern_0;
|
|
|
|
|
case 1:
|
|
|
|
|
return m_pattern_1;
|
|
|
|
|
case 2:
|
|
|
|
|
return m_pattern_2;
|
|
|
|
|
default:
|
|
|
|
|
return m_pattern_3;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Set the specified pattern's weight to the specified value |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
void CExpertSignalAIBase::ApplyPatternWeight(int patternNumber, int weight)
|
|
|
|
|
{
|
|
|
|
|
switch(patternNumber)
|
|
|
|
|
{
|
|
|
|
|
case 0:
|
|
|
|
|
Pattern_0(weight);
|
|
|
|
|
break;
|
|
|
|
|
case 1:
|
|
|
|
|
Pattern_1(weight);
|
|
|
|
|
break;
|
|
|
|
|
case 2:
|
|
|
|
|
Pattern_2(weight);
|
|
|
|
|
break;
|
|
|
|
|
case 3:
|
|
|
|
|
Pattern_3(weight);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| OnTick function |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
void CExpertSignalAIBase::OnTickHandler(void)
|
|
|
|
|
{
|
|
|
|
|
ScheduleTrainingIfNeeded();
|
|
|
|
|
}
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Schedules the next training pass (if one is due) and refreshes |
|
|
|
|
|
//| the per-tick status label. Factored out of OnTickHandler() so |
|
|
|
|
|
//| Warrior_EA.mq5's always-on timer (see PollTraining()) can drive |
|
|
|
|
|
//| this on a fixed wall-clock schedule too - training must not stall |
|
|
|
|
|
//| just because the market is closed and no ticks are arriving. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
void CExpertSignalAIBase::ScheduleTrainingIfNeeded(void)
|
|
|
|
|
{
|
|
|
|
|
//--- stopped: no new training passes get scheduled at all (StartTraining() re-arms this).
|
|
|
|
|
//--- paused: still schedule so bEventStudy/dtStudied bookkeeping stays current, but Train() itself
|
|
|
|
|
//--- blocks at the next era boundary until resumed - keeps in-memory state coherent either way.
|
|
|
|
|
//--- complete: training already converged - a plain new bar must NOT re-enter Train()'s full era
|
|
|
|
|
//--- loop, which would otherwise reset the best-checkpoint/eta-decay tracking and run real
|
|
|
|
|
//--- Net.backProp() passes again, forever, once per bar, on an already-converged model (see
|
|
|
|
|
//--- RefreshConvergedSignal()'s declaration comment). Just keep the live signal current instead.
|
|
|
|
|
//--- publish this signal's current signed confidence for the intelligent trailing (and any other
|
|
|
|
|
//--- live-confidence consumer) - see g_LiveAISignedConfidence in Variables\ConfidenceBridge.mqh.
|
|
|
|
|
//--- Cheap: SignedAIConfidence() just reads the already-computed dPrevSignal.
|
|
|
|
|
g_LiveAISignedConfidence = SignedAIConfidence();
|
|
|
|
|
datetime lastBarDate = (datetime)SeriesInfoInteger(m_symbol.Name(), m_period, SERIES_LASTBAR_DATE);
|
|
|
|
|
// A failed lookup (0) must not silently read as "dtStudied is already caught up, nothing pending" -
|
|
|
|
|
// that would freeze this function into never re-triggering training/signal refresh again until some
|
|
|
|
|
// other path happens to bump dtStudied. Treat a failed lookup as pending instead (same >0-guard
|
|
|
|
|
// philosophy as the SERIES_FIRSTDATE lookup elsewhere in this class) so a transient history-sync
|
|
|
|
|
// hiccup costs one extra harmless check, not a silent stall.
|
|
|
|
|
bool newBarPending = (dPrevSignal == -2 || lastBarDate <= 0 || ((m_inferenceOnly ? m_lastBarTime : dtStudied) < lastBarDate));
|
|
|
|
|
//--- m_inferenceOnly (single backtest) takes the converged/inference branch even if the seeded model
|
|
|
|
|
//--- wasn't flagged complete, so a backtest never drops into Train()'s era loop - see m_inferenceOnly.
|
|
|
|
|
if((m_trainingComplete || m_inferenceOnly) && !m_trainingStopRequested && !m_trainRunActive)
|
|
|
|
|
{
|
|
|
|
|
if(newBarPending)
|
|
|
|
|
RefreshConvergedSignal();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
if(!m_trainingStopRequested && !bEventStudy && newBarPending)
|
|
|
|
|
bEventStudy = EventChartCustom(ChartID(), 1, (long)MathMax(0, MathMin(iTime(m_symbol.Name(), PERIOD_CURRENT, (int)(100 * Net.recentAverageSmoothingFactor * (m_trainingComplete ? 1 : 10))), dtStudied)), 0, "New Bar");
|
|
|
|
|
//--- Train() (see its declaration comment) now yields every ~TRAIN_TIME_BUDGET_MS instead of
|
|
|
|
|
//--- blocking for a whole era, so while a run is active this per-tick line would otherwise
|
|
|
|
|
//--- overwrite Train()'s own full-detail status label on every single tick between chunks -
|
|
|
|
|
//--- flickering between the two instead of showing one steady picture. Only write this terse
|
|
|
|
|
//--- summary when nothing else is actively updating the status label (idle/stopped/paused/cooldown).
|
|
|
|
|
if(!m_trainRunActive)
|
|
|
|
|
{
|
|
|
|
|
//--- Compact, accurate end-state text. The completed state distinguishes a model that is genuinely
|
|
|
|
|
//--- adapting live (online learning active - a live chart with EnableOnlineLearning, not the tester)
|
|
|
|
|
//--- from one running pure inference (the Strategy Tester, or online learning off), so the label is
|
|
|
|
|
//--- literally true either way and never over-promises "keeps learning" when it doesn't - see
|
|
|
|
|
//--- OnlineLearnStep()'s gate for exactly when adaptation runs.
|
|
|
|
|
bool onlineActive = m_enableOnlineLearning && !m_inferenceOnly
|
|
|
|
|
&& !MQLInfoInteger(MQL_TESTER) && !MQLInfoInteger(MQL_OPTIMIZATION) && !MQLInfoInteger(MQL_FORWARD)
|
|
|
|
|
&& CheckPointer(Net) != POINTER_INVALID && !Net.CpuInference();
|
|
|
|
|
//--- Simple end-state panel (default, VerboseMode off): plain-language status + the model's
|
|
|
|
|
//--- compounded/persistent Buy/Sell win-rate (directional accuracy, Neutral excluded - persisted in
|
|
|
|
|
//--- .stats WST5, so it survives a fresh chart reload and is meaningful the moment a drop-and-go user
|
|
|
|
|
//--- attaches the EA) + the current call. The verbose era/forecast dump below stays for power users.
|
|
|
|
|
if(!VerboseMode)
|
|
|
|
|
{
|
|
|
|
|
string statusPlain;
|
|
|
|
|
if(m_trainingComplete)
|
|
|
|
|
statusPlain = onlineActive ? "Live - learning from new bars" : "Ready for live trading";
|
|
|
|
|
else if(m_trainingStopRequested)
|
|
|
|
|
statusPlain = "Paused - progress saved";
|
|
|
|
|
else if(m_trainingPaused)
|
|
|
|
|
statusPlain = "Paused";
|
|
|
|
|
else
|
|
|
|
|
statusPlain = "Getting ready...";
|
|
|
|
|
ENUM_SIGNAL liveSig = DoubleToSignal(dPrevSignal);
|
|
|
|
|
string liveSigPlain = (liveSig == Buy) ? "Buy" : (liveSig == Sell) ? "Sell" : "Neutral (no trade)";
|
|
|
|
|
string simpleLive = DisplayName() + " - " + statusPlain + "\n";
|
|
|
|
|
//--- Only show the accuracy line once at least one signal has been validated (compounded counts
|
|
|
|
|
//--- persist across restarts, so a deployed model shows real numbers immediately, not "measuring").
|
|
|
|
|
if(m_cumIsTotal > 0 || m_cumOosTotal > 0)
|
|
|
|
|
simpleLive += ComputeCompoundedAccuracyLine() + "\n";
|
|
|
|
|
simpleLive += "Current signal: " + liveSigPlain;
|
|
|
|
|
SetStatusLabel(simpleLive);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
string completeText = onlineActive ? "Complete - live (adapting to new bars)" : "Complete - ready for live (inference)";
|
|
|
|
|
string trainingState = m_trainingStopRequested
|
|
|
|
|
? (m_trainingComplete ? completeText : "Stopped - resumable (weights kept)")
|
|
|
|
|
: (m_trainingPaused ? "Paused" : (m_trainingComplete ? completeText : "In progress"));
|
|
|
|
|
//--- same "Forecast: <signal> -> <value>" line the active training loop's status label ends on
|
|
|
|
|
//--- (see the classLine-terminated StringFormat below), instead of a raw bEventStudy/dPrevSignal/
|
|
|
|
|
//--- dtStudied debug dump - this is what stays on screen once training stops/pauses/completes.
|
|
|
|
|
SetStatusLabel(StringFormat(
|
|
|
|
|
ID + " : Era %d -> Training %s\n" +
|
|
|
|
|
"Forecast: %s -> %.2f",
|
|
|
|
|
m_eraCount, trainingState,
|
|
|
|
|
EnumToString(DoubleToSignal(dPrevSignal)), dPrevSignal));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Timer-driven equivalent of OnTickHandler()'s scheduling, called |
|
|
|
|
|
//| from Warrior_EA.mq5's always-on OnTimer() so training keeps |
|
|
|
|
|
//| progressing purely on wall-clock time - no dependency on ticks, |
|
|
|
|
|
//| which simply don't arrive while the market is closed. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
void CExpertSignalAIBase::PollTraining(void)
|
|
|
|
|
{
|
|
|
|
|
//--- Drain a slice of the queued chart-arrow restore FIRST, and skip training work on any tick where
|
|
|
|
|
//--- restoring is still in flight. Both compete for the one MQL5 thread; letting the arrows finish
|
|
|
|
|
//--- quickly (a few hundred ms of slices) means the user sees a complete chart almost immediately,
|
|
|
|
|
//--- whereas interleaving them with 80ms training chunks would stretch the restore over minutes.
|
|
|
|
|
if(m_arrowRestorePending)
|
|
|
|
|
{
|
|
|
|
|
AdvanceChartSignalRestore();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
//--- Same one-thread reasoning as the arrow restore above: a manual rescan (Show Signals) also competes
|
|
|
|
|
//--- for the single MQL5 thread, and its per-bar feedForward is real compute rather than a cheap object
|
|
|
|
|
//--- write, so it must finish its own slices before training resumes rather than interleaving with it.
|
|
|
|
|
if(m_rescanPending)
|
|
|
|
|
{
|
|
|
|
|
AdvanceChartSignalRescan();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if(m_isInitialized)
|
|
|
|
|
ScheduleTrainingIfNeeded();
|
|
|
|
|
}
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
void CExpertSignalAIBase::OnChartEventHandler(const int id,
|
|
|
|
|
const long &lparam,
|
|
|
|
|
const double &dparam,
|
|
|
|
|
const string &sparam)
|
|
|
|
|
{
|
|
|
|
|
if(id == 1001)
|
|
|
|
|
{
|
|
|
|
|
TuneIndicatorsAndTrain(lparam);
|
|
|
|
|
bEventStudy = false;
|
|
|
|
|
OnTickHandler();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Claim m_activeFileName for this chart, terminal-wide. |
|
|
|
|
|
//| |
|
|
|
|
|
//| Two charts running the same AIType with the same retrain-affecting|
|
|
|
|
|
//| inputs resolve to the SAME .nnw/.cfg/.stats/checkpoint set. Both |
|
|
|
|
|
//| then train independently and save over each other, so whichever |
|
|
|
|
|
//| writes last wins and the other's eras are discarded - silently, |
|
|
|
|
|
//| because every individual file operation succeeds. A five-chart |
|
|
|
|
|
//| comparison run on 2026-07-29 lost both its HYBRID models this way |
|
|
|
|
|
//| (one chart left at the AIType default), and the only evidence |
|
|
|
|
|
//| anywhere was that model path appearing twice as often in the log. |
|
|
|
|
|
//| |
|
|
|
|
|
//| A terminal-wide global variable is the right lock rather than a |
|
|
|
|
|
//| lock FILE: GlobalVariableTemp() is an atomic create-if-absent, |
|
|
|
|
|
//| and a TEMPORARY variable dies with the terminal, so a crash can |
|
|
|
|
|
//| never leave a stale lock that blocks the next start. Within one |
|
|
|
|
|
//| session a stale entry is still possible (an EA removed without a |
|
|
|
|
|
//| clean deinit), so the owner's chart id is stored and revalidated. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
bool CExpertSignalAIBase::AcquireConfigLock(void)
|
|
|
|
|
{
|
|
|
|
|
//--- FNV-1a over the resolved filename: every retrain-affecting input is already folded into that
|
|
|
|
|
//--- name, so equal names mean genuinely equal configs and nothing else has to be compared. Hashed
|
|
|
|
|
//--- because MQL5 caps global-variable names at 63 characters and the path alone exceeds that.
|
|
|
|
|
uint h = 2166136261;
|
|
|
|
|
int len = StringLen(m_activeFileName);
|
|
|
|
|
for(int i = 0; i < len; i++)
|
|
|
|
|
{
|
|
|
|
|
h ^= (uint)StringGetCharacter(m_activeFileName, i);
|
|
|
|
|
h *= 16777619;
|
|
|
|
|
}
|
|
|
|
|
string name = "WarriorAI_" + m_id + "_" + StringFormat("%08x", h);
|
|
|
|
|
long self = ChartID();
|
|
|
|
|
//--- Atomic: true means it did not exist and is now ours.
|
|
|
|
|
if(GlobalVariableTemp(name))
|
|
|
|
|
{
|
|
|
|
|
GlobalVariableSet(name, (double)self);
|
|
|
|
|
m_configLockName = name;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
long owner = (long)GlobalVariableGet(name);
|
|
|
|
|
//--- Our own entry: this chart is re-initializing after a parameter change or a recompile whose
|
|
|
|
|
//--- OnDeinit never reached ReleaseConfigLock(). Reclaim it instead of refusing to start.
|
|
|
|
|
if(owner == self)
|
|
|
|
|
{
|
|
|
|
|
m_configLockName = name;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
//--- Owner recorded but its chart no longer runs an expert - take the claim over. owner == 0 is
|
|
|
|
|
//--- deliberately NOT treated as stale: it means another instance created the variable microseconds
|
|
|
|
|
//--- ago and has not stamped its id yet, which is a live claim, not a dead one.
|
|
|
|
|
bool ownerAlive = false;
|
|
|
|
|
if(owner != 0)
|
|
|
|
|
{
|
|
|
|
|
long id = ChartFirst();
|
|
|
|
|
while(id >= 0)
|
|
|
|
|
{
|
|
|
|
|
if(id == owner)
|
|
|
|
|
{
|
|
|
|
|
ownerAlive = (StringLen(ChartGetString(id, CHART_EXPERT_NAME)) > 0);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
id = ChartNext(id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if(owner != 0 && !ownerAlive)
|
|
|
|
|
{
|
|
|
|
|
GlobalVariableSet(name, (double)self);
|
|
|
|
|
m_configLockName = name;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
Print(ID + ": REFUSED to start - another chart is already training this exact configuration. Both" +
|
|
|
|
|
" would save into the same files (" + m_activeFileName + ".nnw plus its .cfg/.stats/checkpoints)" +
|
|
|
|
|
" and overwrite each other's progress with no error reported anywhere. Owner: " +
|
|
|
|
|
(owner != 0 ? "chart " + IntegerToString(owner) + " (" + ChartSymbol(owner) + " " +
|
|
|
|
|
EnumToString((ENUM_TIMEFRAMES)ChartPeriod(owner)) + ")" : "another chart, still initializing") +
|
|
|
|
|
". Change AIType or any retrain-affecting input on THIS chart so it trains its own model, or" +
|
|
|
|
|
" remove one of the two charts. Note AIType defaults to " + EnumToString(AI_HYBRID) +
|
|
|
|
|
" - a chart whose AIType was never actually changed lands here.");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Drop this instance's claim (see AcquireConfigLock). |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
void CExpertSignalAIBase::ReleaseConfigLock(void)
|
|
|
|
|
{
|
|
|
|
|
if(StringLen(m_configLockName) == 0)
|
|
|
|
|
return;
|
|
|
|
|
//--- Only delete a claim we still hold: if a later instance took this entry over via the stale-owner
|
|
|
|
|
//--- path above, deleting it here would silently hand the config to a third chart.
|
|
|
|
|
if((long)GlobalVariableGet(m_configLockName) == ChartID())
|
|
|
|
|
GlobalVariableDel(m_configLockName);
|
|
|
|
|
m_configLockName = "";
|
|
|
|
|
}
|
|
|
|
|
#endif
|