forked from mnbvc188199/Warrior_EA
Three changes, all from the same principle: measure what is there before aiming
at it, and never certify a number you do not trade.
1. THE RECALL FLOOR IS DERIVED, AND IT MOVES BELOW CHANCE.
MinRecall=40 was a constant doing a statistical job. Its reference point is the
33.3% recall a zero-skill 3-class model gets on EVERY class, and against that the
constant was accidentally calibrated for exactly one sample size: on USDJPY CONV
(n_eff 195) 40% is chance + 2.0 SE; on SP500 PAI (n_eff 42) the same 40% is chance
+ 0.9 SE. One chart was being held to a bar twice as strict as the other, for no
reason anyone chose.
CollapseRecallFloorPct() computes it per class from that class's own effective
sample - EffectiveSampleSize(), so the overlap deflation the rest of the gates use
applies here too - as chance - EDGE_MIN_SIGMAS x SE. 26.5% at n_eff 195, 18.8% at
n_eff 42.
BELOW chance, deliberately, and this is the substantive change rather than the
arithmetic. This gate's only job is refusing to call a COLLAPSED model converged.
It is not a quality bar; the deploy gate is the quality bar and it is already
rigorous (chance + 2 SE on the deflated sample, Sidak over candidate eras, then the
cross-instrument pooled certificate). A convergence gate that ALSO demands
provably-above-chance recall on all three classes double-counts that job, and it
has failed that way twice here: MinRecall=60 blocked every SP500 H1 run in 2026-07,
and the 40 that replaced it made Neutral structurally unreachable once first-touch
resolution cut Neutral to a 0.65% residue. A floor nothing can reach does not make
a funded account safer, it stops the run converging at all.
Testing significantly BELOW chance instead catches what a fixed 40 was actually
catching - a model that has stopped emitting a class - and cannot become
unreachable by construction. It also fixes the direction the old constant scaled:
it now widens on a thin OOS window, where low recall genuinely cannot be told from
noise, and tightens on a rich one. Today's SP500 PAI (Buy 51 / Sell 18 / Neutral 30)
is still correctly blocked on Sell.
This also resolves a standing contradiction the code half-admitted at the
isBetterEra comment: selection ranks on coverage-weighted PRECISION while
convergence gated on RECALL, so a sparse high-precision abstainer - precisely the
model that could clear the deploy bar - was blocked by the floor.
The era line now PRINTS the derived floor. Anyone comparing these recalls against a
remembered "40" is reading the wrong bar.
2. DETECTABILITY: WHAT THIS CONFIGURATION COULD PROVE, BEFORE IT TRAINS.
The DEPLOY BAR line states the bar. It never said what reaching it would take, and
that is the actionable direction. ReportDetectability() inverts the same identity -
the gate passes when edge >= z x sqrt(p(1-p)/n_eff), so certifying an edge d needs
n_eff >= z^2 p(1-p)/d^2 independent calls, hence L times as many raw ones - and
prints a +2 / +5 / +10pp ladder as required independent calls, raw calls, and share
of the OOS window, marking any rung that needs more than the window holds IMPOSSIBLE.
Every term is a property of the CONFIGURATION - geometry via break-even, horizon via
mean label lifespan, window via oosCutoff - so no amount of training moves any of
them. It fires once, at the first healthy sweep, beside ReportFeatureHealth, for the
same reason: that is the first moment the bar grid, the measured geometry and the
lifespan are real numbers rather than defaults. It gates nothing.
This is the EdgeFinder discipline applied to our own gate: establish what the market
and the measurement design have to offer, then point the net at it - rather than
spending a thousand eras chasing something this OOS window could never certify.
3. AN ARBITRARY MEMBER WAS DRIVING LIVE EXITS AND TRAILING (user-identified).
Every ensemble member ran
g_LiveAISignedConfidence = SignedAIConfidence();
unconditionally, every tick. Last writer wins. Its consumers are the AI early-exit
route (CExpertSignalCustom::LiveSignedConfidence) and TrailingIntelligent - so on a
four-model chart an LSTM entry could be closed, and its stop moved, on the
Perceptron's opinion alone, decided by scheduling order. Not the vote, not a
weighted blend.
Now the mean across registered members, matching how the ensemble actually trades:
the open decision is the weighted-average vote, and an abstaining member contributes
0 and dilutes exactly as it does there. Members still training read 0, so a
half-trained ensemble reads WEAKER rather than louder - the safe direction for an
exit trigger. Deployed and paused members are included, which is the opposite of the
era barrier's exemption rule and correct for the opposite reason: that one asks who
must be waited for, this asks who has an opinion.
Latent today and staying that way for now by choice - Min_Vote_Close ships Disabled
(101, unreachable on both scales it drives) and TrailingStrategy is off, so live
exits are SL/TP only and the certified hold-to-barrier win rate is what actually
gets traded. Fixed now precisely because the plan is to enable vote exits once the
models are accurate, at which point a scheduling-order exit would be both harmful
and very hard to see.
STILL OPEN, and needs a decision before vote exits go on: the member gate and the
ensemble vote gate both grade hold-to-barrier, so enabling vote exits makes the
certified number stop describing the traded one. Warrior_EA.mq5 currently argues
barrier models may keep vote exits because "their label IS the vote's own horizon" -
that does not hold, since a vote flip at bar 5 of a 64-bar horizon is not the
target-before-stop outcome the gate measured. Either grade the OOS call on the real
exit rule (first of SL / TP / vote-flip / horizon) through the fill engine, or set
HoldToBarrier for ensemble members so the policy cannot drift from the certificate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2367 lines
133 KiB
MQL5
2367 lines
133 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| Warrior_EA |
|
|
//| AnimateDread |
|
|
//| |
|
|
//| Indicator creation and the per-bar input feature vector. |
|
|
//| |
|
|
//| PARTIAL IMPLEMENTATION FILE - not standalone. |
|
|
//| This holds CExpertSignalAIBase method BODIES only. The class |
|
|
//| declaration lives in Expert\ExpertSignalAIBase.mqh, which |
|
|
//| #includes this file at the bottom, after the declaration. Do not |
|
|
//| include it anywhere else and do not compile it on its own. |
|
|
//| |
|
|
//| Split out purely to make the 8216-line original navigable; the |
|
|
//| code inside was moved verbatim, not rewritten. |
|
|
//+------------------------------------------------------------------+
|
|
#ifndef WARRIOR_AIBASE_FEATURES_MQH
|
|
#define WARRIOR_AIBASE_FEATURES_MQH
|
|
//--- Plausibility ceiling for any single input value, enforced once over the whole bar at the end of
|
|
//--- BufferTempDataCompute(). Deliberately far above every clamp used inside that function (the widest
|
|
//--- is +/-10) - this is not a normalization knob, it is the "no legitimate feature looks like this"
|
|
//--- line. See the sanitize loop at the end of BufferTempDataCompute() for what it protects.
|
|
#define FEATURE_ABS_MAX 1.0e4
|
|
//+------------------------------------------------------------------+
|
|
//| Rebuilds only the enabled AD* CiCustom handles in place, so a new |
|
|
//| trial's member-struct param values take effect. Re-Create()-ing |
|
|
//| the existing CiCustom object (rather than removing/re-adding it |
|
|
//| to indicators) avoids adding the same pointer into the CIndicators|
|
|
//| collection twice, which would risk it being deleted twice on |
|
|
//| teardown - MQL5's CIndicators has no documented single-item |
|
|
//| remove. |
|
|
//| |
|
|
//| This used to end "...and CiCustom.Create() already releases its |
|
|
//| old handle." IT DOES NOT, and that sentence cost two models. See |
|
|
//| the handle-release block in the definition below. |
|
|
//+------------------------------------------------------------------+
|
|
//+------------------------------------------------------------------+
|
|
//| See the declaration. Minimum over the enabled tunable indicators, |
|
|
//| because the feature vector is only as ready as its least-ready |
|
|
//| component; -1 when nothing tunable is switched on. |
|
|
//+------------------------------------------------------------------+
|
|
int CExpertSignalAIBase::TunableBarsCalculated(int &enabled)
|
|
{
|
|
enabled = 0;
|
|
int worst = INT_MAX;
|
|
if(m_useMA)
|
|
{
|
|
enabled++;
|
|
worst = (int)MathMin(worst, m_MA.BarsCalculated());
|
|
}
|
|
if(m_useRSI)
|
|
{
|
|
enabled++;
|
|
worst = (int)MathMin(worst, m_RSI.BarsCalculated());
|
|
}
|
|
if(m_useMACD)
|
|
{
|
|
enabled++;
|
|
worst = (int)MathMin(worst, m_MACDFeature.BarsCalculated());
|
|
}
|
|
if(m_useIchimoku)
|
|
{
|
|
enabled++;
|
|
worst = (int)MathMin(worst, m_Ichimoku.BarsCalculated());
|
|
}
|
|
if(m_useADCumulativeDelta)
|
|
{
|
|
enabled++;
|
|
worst = (int)MathMin(worst, m_ADCumulativeDelta.BarsCalculated());
|
|
}
|
|
if(m_useADShorteningOfThrust)
|
|
{
|
|
enabled++;
|
|
worst = (int)MathMin(worst, m_ADShorteningOfThrust.BarsCalculated());
|
|
}
|
|
if(m_useADWyckoffEventStream)
|
|
{
|
|
enabled++;
|
|
worst = (int)MathMin(worst, m_ADWyckoffEventStream.BarsCalculated());
|
|
}
|
|
if(m_useADWyckoffFailedStructure)
|
|
{
|
|
enabled++;
|
|
worst = (int)MathMin(worst, m_ADWyckoffFailedStructure.BarsCalculated());
|
|
}
|
|
if(m_useADWyckoffSignificantBarInversion)
|
|
{
|
|
enabled++;
|
|
worst = (int)MathMin(worst, m_ADWyckoffSignificantBarInversion.BarsCalculated());
|
|
}
|
|
return (worst == INT_MAX) ? -1 : worst;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Back-compatible form for the callers that only want the number. |
|
|
//+------------------------------------------------------------------+
|
|
int CExpertSignalAIBase::TunableBarsCalculated(void)
|
|
{
|
|
int enabled = 0;
|
|
return TunableBarsCalculated(enabled);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| See the declaration. THE one place that decides how much history |
|
|
//| may be asked of the indicators; every ResizeBuffers() call site |
|
|
//| goes through it. |
|
|
//+------------------------------------------------------------------+
|
|
int CExpertSignalAIBase::ServableBars(int want, string context)
|
|
{
|
|
if(want <= 0)
|
|
return want;
|
|
int enabled = 0;
|
|
int servable = TunableBarsCalculated(enabled);
|
|
//--- THE BLIND SPOT THAT COST 2026-08-17 (fixed the same day, after the fact). This used to read
|
|
//--- `if(servable <= 0 || servable >= want) return want;` - one branch covering three completely
|
|
//--- different states, and returning silently from all of them:
|
|
//--- enabled == 0 -> no tunable indicator is on, so there is genuinely no cap. Fine.
|
|
//--- enabled > 0, servable == -1 -> a handle answered INVALID. CopyBuffer will fail at every index.
|
|
//--- enabled > 0, servable == 0 -> a handle is created but has calculated nothing.
|
|
//--- The last two are the exact state the whole depth investigation was looking for, and the gate
|
|
//--- built to find it returned `want` without printing a character. That is why 27 MB of journal
|
|
//--- from a build that HAD this instrumentation contained zero depth lines while two charts sat at
|
|
//--- ok=0/failed=50163 for 38 minutes: the instrument's one silent path was the state it was hunting.
|
|
//--- The dead case is now REPORTED and REPAIRED. This function still answers `want` either way, so
|
|
//--- its own contract ("want, clamped to what the indicators can serve") is unchanged for live
|
|
//--- inference and online learning, which have their own refusal paths. Only SettledBars() - the
|
|
//--- training sweep's entry point, the one caller that can afford to wait - converts the same state
|
|
//--- into a HOLD. See the note at its own dead-handle branch for why the two deliberately differ.
|
|
if(enabled == 0 || servable >= want)
|
|
{
|
|
//--- Cleared on the healthy path too, not only on the clamp path below: a handle that recovers
|
|
//--- all the way to full depth would otherwise leave the latch set and a LATER outage would be
|
|
//--- swallowed - which is the failure mode this whole function is being fixed for.
|
|
m_indicatorDepthDeadWarned = false;
|
|
return want;
|
|
}
|
|
if(servable <= 0)
|
|
{
|
|
//--- REPORT FIRST, THEN REPAIR - in that order, so the depths in this line are the ones that
|
|
//--- caused it. Repairing first would print the freshly-created handle's numbers next to a
|
|
//--- message about a dead one, which is exactly the kind of self-contradicting log line that
|
|
//--- makes the next reader distrust the whole file.
|
|
if(!m_indicatorDepthDeadWarned)
|
|
{
|
|
m_indicatorDepthDeadWarned = true;
|
|
PrintFormat("%s: TUNABLE INDICATOR REPORTS NO CALCULATED BARS - %d tunable indicator(s) enabled"
|
|
" and the least-ready answers BarsCalculated()=%d while %s asked for %d. -1 means an"
|
|
" INVALID HANDLE, 0 means created-but-never-calculated; either way CopyBuffer fails"
|
|
" at EVERY index, the buffer holds nothing, and every feature block that reads it"
|
|
" rejects every bar. This is NOT the depth cap below (that one clamps and trains on"
|
|
" what is servable) - there is nothing to clamp to. Per-indicator depth:%s",
|
|
ID, enabled, servable, context, want, IndicatorDepthReport());
|
|
}
|
|
//--- A dead handle answers EMPTY_VALUE at every index, so a 50k-bar pass over it is 50k guaranteed
|
|
//--- rejections followed by a discarded era, forever - the exact loop that froze USDJPY and
|
|
//--- XAUUSD. Rate-limited inside, and it only touches handles reporting < 0, so a merely COLD
|
|
//--- indicator (valid handle, 0 bars calculated) is left alone to warm up normally.
|
|
RepairDeadIndicatorHandles();
|
|
return want;
|
|
}
|
|
m_indicatorDepthDeadWarned = false;
|
|
//--- -(m_historyBars + 2): the deepest window slot reads (r + m_historyBars - 1), and the MA block
|
|
//--- one further back again for its bar-over-bar change, so the last usable anchor sits that far
|
|
//--- inside the buffer. Without the margin the clamp would hand back a depth whose own deepest
|
|
//--- window still reads off the end - the exact failure it exists to prevent.
|
|
int capped = servable - ((int)m_historyBars + 2);
|
|
if(capped < 0)
|
|
capped = 0;
|
|
//--- Depends only on `servable` and m_historyBars, never on `want`, so it is stable across call
|
|
//--- sites and this logs once per real change rather than once per era per context.
|
|
if(m_indicatorDepthCapBars != capped)
|
|
{
|
|
m_indicatorDepthCapBars = capped;
|
|
PrintFormat("%s: indicator history CAPPED to %d bars (%s asked for %d) - the price series has"
|
|
" that much, but the least-ready tunable indicator has only calculated %d. Past what"
|
|
" an indicator has calculated CopyBuffer does not short-read, it FAILS, so the buffer"
|
|
" holds NOTHING and EVERY index reads EMPTY_VALUE - indistinguishable from a cold"
|
|
" indicator. Per-indicator depth:%s",
|
|
ID, capped, context, want, servable, IndicatorDepthReport());
|
|
}
|
|
return capped;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| See the declaration. ServableBars() with the WAIT in front of it. |
|
|
//+------------------------------------------------------------------+
|
|
int CExpertSignalAIBase::SettledBars(int want, string context)
|
|
{
|
|
if(want <= 0)
|
|
return want;
|
|
int enabled = 0;
|
|
int servable = TunableBarsCalculated(enabled);
|
|
//--- Same three-states-one-branch defect ServableBars() carried (see the long note there): `servable
|
|
//--- < 0` was read as "nothing tunable is on", but it is ALSO what a dead handle answers. Settling a
|
|
//--- dead handle is pointless - it will never climb - so this still falls straight through, but only
|
|
//--- ServableBars() gets to decide that now, and it reports the case on the way past.
|
|
if(enabled == 0 || servable >= want)
|
|
{
|
|
m_depthSettleStart = 0;
|
|
m_depthProbeStable = 0;
|
|
m_depthProbeLast = 0;
|
|
return want;
|
|
}
|
|
if(servable <= 0)
|
|
{
|
|
m_depthSettleStart = 0;
|
|
m_depthProbeStable = 0;
|
|
m_depthProbeLast = 0;
|
|
//--- Routed through ServableBars() rather than answering here, and that detour is the whole point:
|
|
//--- the training sweep - the ONLY caller that reaches the dead-handle state in practice - calls
|
|
//--- SettledBars, not ServableBars. Returning directly is what kept the report (and now the
|
|
//--- repair) unreachable from the one path that needed them.
|
|
ServableBars(want, context);
|
|
//--- 0 = HOLD, and this is the one place the two functions deliberately disagree. ServableBars
|
|
//--- must answer `want` for its own callers (live inference and online learning have their own
|
|
//--- refusal paths and a 0 there would read as "no history at all"), but the training sweep can
|
|
//--- afford to wait and must: nothing is readable at any index right now, so a full-history pass
|
|
//--- would reject every bar, discard the era and start over - the exact 38-minute loop this is
|
|
//--- being fixed for. Whether the handle was just recreated (cold, will climb) or is still dead
|
|
//--- (repair failed), holding is right; Train() reports the hold every minute and the era-barrier
|
|
//--- liveness escape releases the rest of the ensemble if it never resolves.
|
|
return 0;
|
|
}
|
|
uint now = GetTickCount();
|
|
//--- First shortfall: start the clock and let the priming request above do its work. Deliberately
|
|
//--- no sweep this call - a 50k-bar feature sweep is exactly what starves the indicator threads we
|
|
//--- are waiting on, which is how the old loop sustained itself for 40 minutes at a time.
|
|
if(m_depthSettleStart == 0)
|
|
{
|
|
m_depthSettleStart = now;
|
|
m_depthProbeTick = now;
|
|
m_depthProbeLast = servable;
|
|
m_depthProbeStable = 0;
|
|
PrintFormat("%s: PRIMING indicator history for the %s - %d of %d bars calculated so far. Holding"
|
|
" the sweep until the count stops rising (probe every %ds, needs %d steady probes,"
|
|
" gives up after %ds and uses whatever is there). Per-indicator depth:%s",
|
|
ID, context, servable, want, DEPTH_SETTLE_PROBE_MS / 1000,
|
|
DEPTH_SETTLE_STABLE_PROBES, DEPTH_SETTLE_TIMEOUT_MS / 1000, IndicatorDepthReport());
|
|
return 0;
|
|
}
|
|
//--- Unsigned subtraction, so this is correct across GetTickCount()'s 49-day wrap (same idiom as
|
|
//--- m_coldSweepTick's backoff).
|
|
if(now - m_depthProbeTick < DEPTH_SETTLE_PROBE_MS)
|
|
return 0;
|
|
m_depthProbeTick = now;
|
|
if(servable != m_depthProbeLast)
|
|
{
|
|
//--- STILL MOVING. Growing is the terminal working through the history; shrinking happens when a
|
|
//--- handle is rebuilt under us and starts over. Either way it is not settled, so the streak
|
|
//--- restarts rather than counting a change as a steady observation.
|
|
PrintFormat("%s: priming %s - %d of %d bars (was %d), still moving", ID, context, servable, want,
|
|
m_depthProbeLast);
|
|
m_depthProbeLast = servable;
|
|
m_depthProbeStable = 0;
|
|
return 0;
|
|
}
|
|
m_depthProbeStable++;
|
|
bool steady = (m_depthProbeStable >= DEPTH_SETTLE_STABLE_PROBES);
|
|
bool expired = ((now - m_depthSettleStart) >= DEPTH_SETTLE_TIMEOUT_MS);
|
|
if(!steady && !expired)
|
|
return 0;
|
|
//--- Settled (or waited long enough) BELOW what was asked. This is the real depth, not a snapshot of
|
|
//--- a value still climbing, so it is now safe to clamp to it and get on with training.
|
|
PrintFormat("%s: priming %s DONE - depth settled at %d of %d bars after %ds%s. Training proceeds on"
|
|
" the %d bars the indicators can actually serve.",
|
|
ID, context, servable, want, (int)((now - m_depthSettleStart) / 1000),
|
|
expired && !steady ? " (gave up waiting - it never went steady)" : "", servable);
|
|
m_depthSettleStart = 0;
|
|
m_depthProbeStable = 0;
|
|
m_depthProbeLast = 0;
|
|
return ServableBars(want, context);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| See the declaration. What this configuration would have to FIRE |
|
|
//| before any edge of a given size becomes certifiable. |
|
|
//+------------------------------------------------------------------+
|
|
void CExpertSignalAIBase::ReportDetectability(int oosBars)
|
|
{
|
|
if(m_detectabilityReported || oosBars <= 0)
|
|
return;
|
|
m_detectabilityReported = true;
|
|
double p = CostAdjustedBreakEvenPct() / 100.0;
|
|
if(p <= 0.0 || p >= 1.0)
|
|
return;
|
|
double L = MeanLabelLifespan();
|
|
//--- Invert the deploy gate. It passes when edge >= z * sqrt(p(1-p)/n_eff), so for a hypothesised
|
|
//--- true edge d it needs n_eff >= z^2 p(1-p) / d^2 independent calls - and since overlapping
|
|
//--- labels are worth ~1/L each, that is L times as many RAW calls. Everything on the right-hand
|
|
//--- side is a property of the CONFIGURATION (geometry via p, horizon via L, window via oosBars),
|
|
//--- not of the model, which is the whole point: no amount of training moves it.
|
|
string ladder = "";
|
|
double edges[3] = {2.0, 5.0, 10.0};
|
|
for(int i = 0; i < 3; i++)
|
|
{
|
|
double d = edges[i] / 100.0;
|
|
double needEff = (EDGE_MIN_SIGMAS * EDGE_MIN_SIGMAS) * p * (1.0 - p) / (d * d);
|
|
double needRaw = needEff * L;
|
|
double needCoverage = 100.0 * needRaw / (double)oosBars;
|
|
ladder += StringFormat(" %+.0fpp:%.0f indep=%.0f calls=%.0f%% of window%s |",
|
|
edges[i], needEff, needRaw, needCoverage,
|
|
needCoverage > 100.0 ? " IMPOSSIBLE" : "");
|
|
}
|
|
PrintFormat("%s: DETECTABILITY of this configuration (break-even %.1f%%, mean label lifespan %.1f"
|
|
" bars, OOS window %d bars) - to certify an edge of X the gate needs:%s"
|
|
" Read it as a budget, not a target: these are properties of the GEOMETRY, the HORIZON"
|
|
" and the WINDOW, so a better model cannot change any of them. Where a rung says"
|
|
" IMPOSSIBLE, no win rate this model could ever produce would clear the deploy bar on"
|
|
" this window - the answer there is more instruments, a lower timeframe or a narrower"
|
|
" barrier, never more eras. Coverage is also not free in the other direction: firing on"
|
|
" more bars buys independent calls at the cost of precision, so the reachable band is"
|
|
" bounded at both ends.",
|
|
ID, 100.0 * p, L, oosBars, ladder);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| See the declaration. The per-class COLLAPSE floor, derived rather |
|
|
//| than configured. |
|
|
//+------------------------------------------------------------------+
|
|
double CExpertSignalAIBase::CollapseRecallFloorPct(int classTrueCount)
|
|
{
|
|
//--- Chance recall is 1/K for K classes and does not depend on the class priors: a zero-skill model
|
|
//--- that emits class c with probability q gets recall q on EVERY true class, and the uniform
|
|
//--- zero-skill model has q = 1/K. That, not a configured constant, is the reference this floor is
|
|
//--- measured against.
|
|
double chance = 100.0 / 3.0;
|
|
double effN = EffectiveSampleSize((double)classTrueCount);
|
|
if(effN <= 0.0)
|
|
return (double)m_minDirectionalRecallPct;
|
|
double q = 1.0 / 3.0;
|
|
double se = 100.0 * MathSqrt(q * (1.0 - q) / effN);
|
|
double floorPct = chance - EDGE_MIN_SIGMAS * se;
|
|
//--- Never negative, and never so high it becomes the unreachable bar this replaced.
|
|
if(floorPct < 0.0)
|
|
floorPct = 0.0;
|
|
return floorPct;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| RE-CREATE any enabled tunable indicator whose handle has gone |
|
|
//| INVALID underneath us. See the declaration for the evidence. |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::RepairDeadIndicatorHandles(void)
|
|
{
|
|
if(m_indicatorsPtr == NULL)
|
|
return false;
|
|
uint now = GetTickCount();
|
|
//--- Cooldown, because every consumer of ServableBars() can reach this - the training sweep, live
|
|
//--- inference on every tick, online learning - and a repair storm against a terminal that is
|
|
//--- genuinely refusing to create the indicator would be worse than the outage it is fixing.
|
|
if(m_handleRepairTick != 0 && now - m_handleRepairTick < HANDLE_REPAIR_COOLDOWN_MS)
|
|
return false;
|
|
m_handleRepairTick = now;
|
|
//--- NOT released first, deliberately. BarsCalculated() answering -1 means the terminal no longer
|
|
//--- knows this handle, so there is nothing to give back - and MT5 recycles handle VALUES, so
|
|
//--- releasing a stale one risks decrementing whatever now owns that number. Re-Create only.
|
|
//--- addToCollection is false throughout: the CiCustom/CiMA objects are already in m_indicatorsPtr
|
|
//--- from InitIndicators(), and adding them twice would let the collection delete them twice.
|
|
int repaired = 0;
|
|
string before = IndicatorDepthReport();
|
|
if(m_useMA && m_MA.BarsCalculated() < 0)
|
|
repaired += (InitMA(m_indicatorsPtr, false) ? 1 : 0);
|
|
if(m_useRSI && m_RSI.BarsCalculated() < 0)
|
|
repaired += (InitRSI(m_indicatorsPtr, false) ? 1 : 0);
|
|
if(m_useMACD && m_MACDFeature.BarsCalculated() < 0)
|
|
repaired += (InitMACDFeature(m_indicatorsPtr, false) ? 1 : 0);
|
|
if(m_useIchimoku && m_Ichimoku.BarsCalculated() < 0)
|
|
repaired += (InitIchimoku(m_indicatorsPtr, false) ? 1 : 0);
|
|
if(m_useADCumulativeDelta && m_ADCumulativeDelta.BarsCalculated() < 0)
|
|
repaired += (InitADCumulativeDelta(m_indicatorsPtr, false) ? 1 : 0);
|
|
if(m_useADShorteningOfThrust && m_ADShorteningOfThrust.BarsCalculated() < 0)
|
|
repaired += (InitADShorteningOfThrust(m_indicatorsPtr, false) ? 1 : 0);
|
|
if(m_useADWyckoffEventStream && m_ADWyckoffEventStream.BarsCalculated() < 0)
|
|
repaired += (InitADWyckoffEventStream(m_indicatorsPtr, false) ? 1 : 0);
|
|
if(m_useADWyckoffFailedStructure && m_ADWyckoffFailedStructure.BarsCalculated() < 0)
|
|
repaired += (InitADWyckoffFailedStructure(m_indicatorsPtr, false) ? 1 : 0);
|
|
if(m_useADWyckoffSignificantBarInversion && m_ADWyckoffSignificantBarInversion.BarsCalculated() < 0)
|
|
repaired += (InitADWyckoffSignificantBarInversion(m_indicatorsPtr, false) ? 1 : 0);
|
|
if(repaired == 0)
|
|
return false;
|
|
//--- Every cached feature row was computed against the handle that just got replaced. The values
|
|
//--- themselves are identical (same params, same series) so this is belt-and-braces, but a cache
|
|
//--- keyed to a handle that no longer exists is exactly the kind of thing that survives a fix and
|
|
//--- resurfaces a week later.
|
|
ArrayInitialize(m_featureCacheHasValue, false);
|
|
PrintFormat("%s: RECREATED %d dead indicator handle(s) - the terminal had stopped recognising them,"
|
|
" so CopyBuffer failed at every index and every bar of the sweep was rejected. Depth"
|
|
" BEFORE:%s | AFTER:%s. A freshly created handle calculates asynchronously, so the next"
|
|
" few calls may still report a short depth - that is the normal priming path, not this"
|
|
" fault. If this line repeats on a cycle, something is releasing the handle out from"
|
|
" under this member and the recreate is only papering over it.",
|
|
ID, repaired, before, IndicatorDepthReport());
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| See the declaration. Names WHICH indicator is short, so the next |
|
|
//| occurrence is read off the log instead of inferred. |
|
|
//+------------------------------------------------------------------+
|
|
string CExpertSignalAIBase::IndicatorDepthReport(void)
|
|
{
|
|
string s = StringFormat(" price=%d", Bars(m_symbol.Name(), PERIOD_CURRENT));
|
|
//--- HANDLE VALUES, not just depths. A depth of -1 says "this handle is dead"; the handle NUMBER is
|
|
//--- what says whether it was never created, or was created and later released out from under this
|
|
//--- member - and on an ensemble chart all four members request identical params, so MT5 hands them
|
|
//--- the SAME refcounted handle and one member's release is felt by the others. Comparing the number
|
|
//--- across members' log lines is what distinguishes those cases. Costs nothing to print.
|
|
if(m_useMA)
|
|
s += StringFormat(" MA=%d(h%d)", m_MA.BarsCalculated(), m_MA.Handle());
|
|
if(m_useRSI)
|
|
s += StringFormat(" RSI=%d", m_RSI.BarsCalculated());
|
|
if(m_useMACD)
|
|
s += StringFormat(" MACD=%d", m_MACDFeature.BarsCalculated());
|
|
if(m_useIchimoku)
|
|
s += StringFormat(" Ichi=%d", m_Ichimoku.BarsCalculated());
|
|
if(m_useADCumulativeDelta)
|
|
s += StringFormat(" CumDelta=%d", m_ADCumulativeDelta.BarsCalculated());
|
|
if(m_useADShorteningOfThrust)
|
|
s += StringFormat(" SoT=%d", m_ADShorteningOfThrust.BarsCalculated());
|
|
if(m_useADWyckoffEventStream)
|
|
s += StringFormat(" WES=%d", m_ADWyckoffEventStream.BarsCalculated());
|
|
if(m_useADWyckoffFailedStructure)
|
|
s += StringFormat(" WFS=%d", m_ADWyckoffFailedStructure.BarsCalculated());
|
|
if(m_useADWyckoffSignificantBarInversion)
|
|
s += StringFormat(" WSBI=%d", m_ADWyckoffSignificantBarInversion.BarsCalculated());
|
|
//--- Not tunable, so absent from TunableBarsCalculated() - but the swing block reads it on every bar
|
|
//--- and neutral-fills when it is short, which is silent. Worth seeing next to the others.
|
|
s += StringFormat(" ZigZag=%d ATR=%d", m_ADZigZag.BarsCalculated(), m_ATR.BarsCalculated());
|
|
return s;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//+------------------------------------------------------------------+
|
|
//| Adopt a saved indicator-param set, rebuilding handles only on a |
|
|
//| REAL change. |
|
|
//| |
|
|
//| The resume path restores the params a model was trained with and |
|
|
//| used to call ReInitADIndicators unconditionally. In the common |
|
|
//| case the saved set is byte-identical to the values the indicators |
|
|
//| were created with a few hundred milliseconds earlier (the MI |
|
|
//| tuner usually keeps the configured settings), so the "rebuild" |
|
|
//| destroyed five working, already-calculating indicator instances |
|
|
//| to recreate them with the same inputs - at process start, with |
|
|
//| history still syncing. On a memory-starved box (2026-08-13: |
|
|
//| 1 GB free of 31) the replacements stayed cold for 6+ minutes and |
|
|
//| the resumed model could not train a single era. A no-change adopt |
|
|
//| now only aligns the tuner state and leaves the live instances |
|
|
//| alone. |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::AdoptIndicatorParams(const double &loaded[], CIndicators *indicators)
|
|
{
|
|
double current[];
|
|
m_indicatorTuner.Flatten(current);
|
|
bool changed = (ArraySize(current) != ArraySize(loaded));
|
|
if(!changed)
|
|
for(int k = 0; k < ArraySize(loaded); k++)
|
|
if(current[k] != loaded[k])
|
|
{
|
|
changed = true;
|
|
break;
|
|
}
|
|
//--- the tuner mirrors the model's params either way - it feeds the .nnw save and the fingerprint
|
|
m_indicatorTuner.Unflatten(loaded);
|
|
if(!changed)
|
|
{
|
|
PrintVerbose(ID + ": saved indicator params match the live indicators - keeping the existing"
|
|
" instances (no handle rebuild).");
|
|
return true;
|
|
}
|
|
Print(ID + ": saved indicator params differ from the live defaults - rebuilding the tunable"
|
|
" indicator handles to match the model they trained.");
|
|
return ReInitADIndicators(indicators);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::ReInitADIndicators(CIndicators *indicators)
|
|
{
|
|
bool result = true;
|
|
//--- RELEASE THE HANDLE EACH Create() IS ABOUT TO REPLACE. Without this every call here leaks one
|
|
//--- terminal-side indicator instance per enabled indicator, and this function is the tuner's inner
|
|
//--- loop - AutoTuneIndicators scored 324 candidates on SP500 H1, so ~324 x 6 orphaned instances per
|
|
//--- model, each holding a full-history buffer set (ADWyckoffEventStream is 14 buffers x ~38k bars x
|
|
//--- 8 bytes = ~4.3 MB EACH). That is gigabytes, and it is what killed CONV and LSTM on 2026-08-07:
|
|
//--- 6664 and 2048 "VirtualAlloc failed in large allocator" lines in the terminal journal, then
|
|
//--- "expert Warrior_EA (SP500,H1) removed", 50ms and 71ms after each finished its sweep and era 0
|
|
//--- tried to allocate. HYBRID only survived because those two died first and freed the memory.
|
|
//---
|
|
//--- THE COMMENT THAT USED TO SIT HERE SAID Create() "already releases its old handle". It does not.
|
|
//--- MQL5's CIndicator::Create (Include\Indicators\Indicator.mqh) is:
|
|
//--- m_handle = IndicatorCreate(symbol, period, type, num_params, params);
|
|
//--- - a plain overwrite. Its only success-path IndicatorRelease is in ~CIndicator. Nothing else in
|
|
//--- this codebase called IndicatorRelease at all.
|
|
//---
|
|
//--- UNCONDITIONAL, not "only when the handle changed". MT5 refcounts indicator instances by
|
|
//--- (symbol, period, params): re-creating with IDENTICAL params hands back the SAME handle with the
|
|
//--- count incremented, so skipping the release there would leak a reference just as surely - which is
|
|
//--- the "23 x WFS(48,3,1.80,1.10)" pattern in the journal, next to the distinct-parameter leaks from
|
|
//--- the candidate grid. Either way Create() added exactly one reference and we still hold exactly one
|
|
//--- handle, so exactly one release is owed.
|
|
int hCD = m_useADCumulativeDelta ? m_ADCumulativeDelta.Handle() : INVALID_HANDLE;
|
|
int hSOT = m_useADShorteningOfThrust ? m_ADShorteningOfThrust.Handle() : INVALID_HANDLE;
|
|
int hWES = m_useADWyckoffEventStream ? m_ADWyckoffEventStream.Handle() : INVALID_HANDLE;
|
|
int hWFS = m_useADWyckoffFailedStructure ? m_ADWyckoffFailedStructure.Handle() : INVALID_HANDLE;
|
|
int hWSBI = m_useADWyckoffSignificantBarInversion ? m_ADWyckoffSignificantBarInversion.Handle() : INVALID_HANDLE;
|
|
int hMA = m_useMA ? m_MA.Handle() : INVALID_HANDLE;
|
|
int hRSI = m_useRSI ? m_RSI.Handle() : INVALID_HANDLE;
|
|
int hMACD = m_useMACD ? m_MACDFeature.Handle() : INVALID_HANDLE;
|
|
int hIchi = m_useIchimoku ? m_Ichimoku.Handle() : INVALID_HANDLE;
|
|
if(m_useADCumulativeDelta)
|
|
result = InitADCumulativeDelta(indicators, false) && result;
|
|
if(m_useADShorteningOfThrust)
|
|
result = InitADShorteningOfThrust(indicators, false) && result;
|
|
if(m_useADWyckoffEventStream)
|
|
result = InitADWyckoffEventStream(indicators, false) && result;
|
|
if(m_useADWyckoffFailedStructure)
|
|
result = InitADWyckoffFailedStructure(indicators, false) && result;
|
|
if(m_useADWyckoffSignificantBarInversion)
|
|
result = InitADWyckoffSignificantBarInversion(indicators, false) && result;
|
|
if(m_useMA)
|
|
result = InitMA(indicators, false) && result;
|
|
if(m_useRSI)
|
|
result = InitRSI(indicators, false) && result;
|
|
if(m_useMACD)
|
|
result = InitMACDFeature(indicators, false) && result;
|
|
if(m_useIchimoku)
|
|
result = InitIchimoku(indicators, false) && result;
|
|
//--- AFTER the re-creates, never before: releasing first can drop the terminal's last reference and
|
|
//--- make it tear the instance down, so an identical-params Create() would then rebuild it from
|
|
//--- scratch instead of re-using the live one - turning a refcount bump into a full recalculation over
|
|
//--- the whole history, 324 times over. Released here, the old instance survives until its
|
|
//--- replacement exists.
|
|
if(hCD != INVALID_HANDLE)
|
|
IndicatorRelease(hCD);
|
|
if(hSOT != INVALID_HANDLE)
|
|
IndicatorRelease(hSOT);
|
|
if(hWES != INVALID_HANDLE)
|
|
IndicatorRelease(hWES);
|
|
if(hWFS != INVALID_HANDLE)
|
|
IndicatorRelease(hWFS);
|
|
if(hWSBI != INVALID_HANDLE)
|
|
IndicatorRelease(hWSBI);
|
|
if(hMA != INVALID_HANDLE)
|
|
IndicatorRelease(hMA);
|
|
if(hRSI != INVALID_HANDLE)
|
|
IndicatorRelease(hRSI);
|
|
if(hMACD != INVALID_HANDLE)
|
|
IndicatorRelease(hMACD);
|
|
if(hIchi != INVALID_HANDLE)
|
|
IndicatorRelease(hIchi);
|
|
//--- Indicator params just changed, so every cached feature row is now stale (the feature values
|
|
//--- depend on these indicators; the LABELS do not - they come from ADZigZag - so the label cache is
|
|
//--- deliberately left intact and reused). Without this, a tuner candidate would silently train and be
|
|
//--- scored on the PREVIOUS candidate's features. Cheap: just flags rows for lazy recompute on next read.
|
|
ArrayInitialize(m_featureCacheHasValue, false);
|
|
return result;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::ResizeBuffers(int barIndex)
|
|
{
|
|
// The Ichimoku feature's Chikou term reads m_Close at idx + ichiKijun (see its block in
|
|
// BufferTempDataCompute() for why that direction, and only that direction, is lookahead-free), which
|
|
// is further back than any other consumer of the close series reaches. Grow the close buffer to match
|
|
// when that feature is on, so the oldest requested bars resolve from real data instead of tripping
|
|
// that block's EMPTY_VALUE guard and being rejected as unusable examples.
|
|
// CLAMPED TO WHAT EXISTS. CSeries::BufferResize -> CheckLoadHistory -> CheckTerminalHistory only
|
|
// succeeds when Bars() >= size, and Train() calls this with barIndex == Bars() - so ANY block that
|
|
// asks for "barIndex + something" fails the whole ResizeBuffers call rather than just its own deep
|
|
// reads. The MA block proved that on 2026-08-17 with a +1 (see its comment below); the Ichimoku
|
|
// pair here is the same defect with a much larger constant, latent only because that feature has
|
|
// been off. The oldest ichiKijun bars then have no cloud, which the block's EMPTY_VALUE guard
|
|
// already handles as a per-bar rejection - the correct outcome, and cheap next to losing all of it.
|
|
int maxBars = Bars(m_symbol.Name(), PERIOD_CURRENT);
|
|
int closeBars = m_useIchimoku ? (int)MathMin(barIndex + m_indicatorTuner.ichiKijun, maxBars) : barIndex;
|
|
if(!m_Open.BufferResize(barIndex) || !m_Close.BufferResize(closeBars) || !m_High.BufferResize(barIndex) || !m_Low.BufferResize(barIndex))
|
|
return false;
|
|
if(m_useVolumes)
|
|
{
|
|
if(!m_Volumes.BufferResize(barIndex))
|
|
return false;
|
|
}
|
|
// Unconditional - see InitTime()'s call site in InitIndicators() for why m_Time must always be live.
|
|
if(!m_Time.BufferResize(barIndex))
|
|
return false;
|
|
if(m_useMA)
|
|
{
|
|
// NOT barIndex + 1, though the MA block does read GetData(idx) AND GetData(idx + 1) for its
|
|
// bar-over-bar change. That looks like an off-by-one and is not one: Train() calls this with
|
|
// barIndex == Bars(), and CSeries::BufferResize -> CheckLoadHistory -> CheckTerminalHistory
|
|
// only succeeds when Bars() >= size, so asking for one MORE bar than exists fails the whole
|
|
// ResizeBuffers call. Shipped as a "fix" on 2026-08-17 and it stopped every chart dead:
|
|
// "failed to get 50180 bars for USDJPY,PERIOD_H4" (Bars() was 50,179) and 33,983 vs 33,982 on
|
|
// XAUUSD, after which StartLabelCachePrebuild() bailed on the false return - silently, forever,
|
|
// showing only as "arming the first label-cache prebuild".
|
|
// The read at the OLDEST bar is SUPPOSED to fail: there is no older bar to difference against.
|
|
// Rejecting that single bar is correct; buying it costs the entire history.
|
|
if(!m_MA.BufferResize(barIndex))
|
|
return false;
|
|
}
|
|
if(m_useRSI)
|
|
{
|
|
if(!m_RSI.BufferResize(barIndex))
|
|
return false;
|
|
}
|
|
if(m_useMACD)
|
|
{
|
|
if(!m_MACDFeature.BufferResize(barIndex))
|
|
return false;
|
|
}
|
|
if(m_useIchimoku)
|
|
{
|
|
// + m_indicatorTuner.ichiKijun: the cloud reads reach that many bars FURTHER back than every other
|
|
// indicator here does (see the m_useIchimoku feature block for why the offset exists), so sizing
|
|
// this buffer to barIndex alone would leave the oldest requested bars' cloud values unavailable.
|
|
if(!m_Ichimoku.BufferResize((int)MathMin(barIndex + m_indicatorTuner.ichiKijun, maxBars)))
|
|
return false;
|
|
}
|
|
// Unconditional (not gated by m_useATR): the ATR-normalization in BufferTempData() reads
|
|
// m_ATR.Main() regardless of whether ATR is enabled as an explicit extra input feature -
|
|
// m_useATR only controls that feature-count opt-in (see InitIndicators()'s "already init in the
|
|
// base class" comment), not whether ATR data itself needs to be kept live.
|
|
if(!m_ATR.BufferResize(barIndex))
|
|
return false;
|
|
// Unconditional, same reasoning as m_ATR above - m_ADZigZag drives the swing-context features AND
|
|
// ComputeBarrierHorizonBars()'s measurement, not an opt-in feature, so it's never gated by an
|
|
// m_use* flag. (It was also the training-label source until the 2026-08-01 triple-barrier relabel.)
|
|
if(!m_ADZigZag.BufferResize(barIndex))
|
|
return false;
|
|
if(m_useADCumulativeDelta)
|
|
{
|
|
if(!m_ADCumulativeDelta.BufferResize(barIndex))
|
|
return false;
|
|
}
|
|
if(m_useADShorteningOfThrust)
|
|
{
|
|
if(!m_ADShorteningOfThrust.BufferResize(barIndex))
|
|
return false;
|
|
}
|
|
if(m_useADWyckoffEventStream)
|
|
{
|
|
if(!m_ADWyckoffEventStream.BufferResize(barIndex))
|
|
return false;
|
|
}
|
|
if(m_useADWyckoffFailedStructure)
|
|
{
|
|
if(!m_ADWyckoffFailedStructure.BufferResize(barIndex))
|
|
return false;
|
|
}
|
|
if(m_useADWyckoffSignificantBarInversion)
|
|
{
|
|
if(!m_ADWyckoffSignificantBarInversion.BufferResize(barIndex))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::RefreshData()
|
|
{
|
|
// CSeries/CIndicator::Refresh() is void - there is no per-call success/failure signal to
|
|
// propagate here. The real data-validity check happens downstream, per value, in
|
|
// BufferTempDataCompute() (EMPTY_VALUE / atr<=0 guards) - this function's job is only to ask
|
|
// every buffer to refresh, unconditionally, before that per-value check runs.
|
|
m_Open.Refresh(OBJ_ALL_PERIODS);
|
|
m_Close.Refresh(OBJ_ALL_PERIODS);
|
|
m_High.Refresh(OBJ_ALL_PERIODS);
|
|
m_Low.Refresh(OBJ_ALL_PERIODS);
|
|
if(m_useVolumes)
|
|
{
|
|
m_Volumes.Refresh(OBJ_ALL_PERIODS);
|
|
}
|
|
// Unconditional - see InitTime()'s call site in InitIndicators() for why m_Time must always be live.
|
|
m_Time.Refresh(OBJ_ALL_PERIODS);
|
|
if(m_useMA)
|
|
{
|
|
m_MA.Refresh(OBJ_ALL_PERIODS);
|
|
}
|
|
if(m_useRSI)
|
|
{
|
|
m_RSI.Refresh(OBJ_ALL_PERIODS);
|
|
}
|
|
if(m_useMACD)
|
|
{
|
|
m_MACDFeature.Refresh(OBJ_ALL_PERIODS);
|
|
}
|
|
if(m_useIchimoku)
|
|
{
|
|
m_Ichimoku.Refresh(OBJ_ALL_PERIODS);
|
|
}
|
|
// Unconditional - see the matching BufferResize() comment above.
|
|
m_ATR.Refresh(OBJ_ALL_PERIODS);
|
|
m_ADZigZag.Refresh(OBJ_ALL_PERIODS);
|
|
if(m_useADCumulativeDelta)
|
|
{
|
|
m_ADCumulativeDelta.Refresh(OBJ_ALL_PERIODS);
|
|
}
|
|
if(m_useADShorteningOfThrust)
|
|
{
|
|
m_ADShorteningOfThrust.Refresh(OBJ_ALL_PERIODS);
|
|
}
|
|
if(m_useADWyckoffEventStream)
|
|
{
|
|
m_ADWyckoffEventStream.Refresh(OBJ_ALL_PERIODS);
|
|
}
|
|
if(m_useADWyckoffFailedStructure)
|
|
{
|
|
m_ADWyckoffFailedStructure.Refresh(OBJ_ALL_PERIODS);
|
|
}
|
|
if(m_useADWyckoffSignificantBarInversion)
|
|
{
|
|
m_ADWyckoffSignificantBarInversion.Refresh(OBJ_ALL_PERIODS);
|
|
}
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
//+------------------------------------------------------------------+
|
|
//| Cache-or-compute wrapper around BufferTempDataCompute(): a given |
|
|
//| now-relative bar index's feature vector is invariant until the |
|
|
//| next candle close (see m_featureCache's declaration comment), so |
|
|
//| a cache hit just replays the m_neuronsCount values already |
|
|
//| computed for this idx straight into TempData instead of re- |
|
|
//| deriving them from price/ATR/AD-indicator buffers again. |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::BufferTempData(int idx)
|
|
{
|
|
int width = m_neuronsCount;
|
|
bool cacheable = (idx >= 0 && idx < ArraySize(m_featureCacheHasValue) && width > 0);
|
|
if(cacheable && m_featureCacheHasValue[idx])
|
|
{
|
|
if(!m_featureCacheValid[idx])
|
|
return false;
|
|
int base = idx * width;
|
|
for(int f = 0; f < width; f++)
|
|
if(!TempData.Add(m_featureCache[base + f]))
|
|
return false;
|
|
return true;
|
|
}
|
|
int startTotal = TempData.Total();
|
|
bool ok = BufferTempDataCompute(idx);
|
|
//--- WIDTH CONTRACT. Every enabled feature block must emit exactly the number of values
|
|
//--- m_neuronsCount was computed from, on every bar, unconditionally - a block that emits its
|
|
//--- values on some bars and skips them on others (because an indicator, panel or series was
|
|
//--- unavailable for THAT bar) does not merely shorten the window: it SHIFTS every feature after it
|
|
//--- into the wrong slot, and the net then trains on silently misaligned inputs that still look like
|
|
//--- a valid window to everything downstream. Caught here rather than left to surface as
|
|
//--- BuildFeatureWindow's length check, which cannot say which bar or which block was responsible.
|
|
if(ok)
|
|
{
|
|
int produced = TempData.Total() - startTotal;
|
|
if(produced != width)
|
|
{
|
|
ok = false;
|
|
m_featureFailTransient = false; // a width fault is structural, never "not ready yet"
|
|
if(!m_featureWidthWarned)
|
|
{
|
|
m_featureWidthWarned = true;
|
|
PrintFormat("%s: FEATURE WIDTH MISMATCH at bar %d - the enabled blocks produced %d values"
|
|
" but m_neuronsCount says %d. Every feature after the short block would have"
|
|
" landed in the wrong slot, so the bar is rejected rather than trained on."
|
|
" A block that can be conditionally unavailable must emit neutral values, not"
|
|
" nothing. Check the optional blocks first (cross-asset XA, spread SPR, swing"
|
|
" context) - those are the ones with an availability test.",
|
|
ID, idx, produced, width);
|
|
}
|
|
//--- Roll back the partial bar so the caller's window cannot contain half of it.
|
|
while(TempData.Total() > startTotal)
|
|
TempData.Delete(TempData.Total() - 1);
|
|
}
|
|
}
|
|
//--- ONLY SUCCESSES ARE CACHED. A miss is never stored, in any form.
|
|
//---
|
|
//--- The previous rule cached a miss whenever it was not flagged transient, and flagged exactly TWO
|
|
//--- guards - the EMPTY_VALUE open and the cold ATR. That was the half-fix: every OTHER rejection in
|
|
//--- BufferTempDataCompute (an indicator buffer not yet calculated, a panel not yet built, a series
|
|
//--- not yet loaded, a failed Add) still cached as PERMANENT, so one early sweep across cold
|
|
//--- indicators poisoned those bars for the rest of the process. Observed 2026-08-11: the MI
|
|
//--- pre-scan runs ~3 s after OnInit, touches all 54k bars while the indicators are still warming,
|
|
//--- and the run then reported "0 samples" and never trained again - the same failure the two-guard
|
|
//--- version was written to prevent, arriving through the guards it did not cover.
|
|
//---
|
|
//--- Enumerating which rejections are "really" permanent is the wrong shape of fix: it is a list that
|
|
//--- has to be re-audited every time a feature block is added, and being wrong once costs the whole
|
|
//--- run silently. Caching only successes needs no list and cannot be wrong. The cost is bounded and
|
|
//--- small: in steady state the only bars that still fail are the handful at the deep end of history
|
|
//--- inside the indicators' own warm-up, so an era recomputes ~ind_Periods bars rather than 54k.
|
|
//---
|
|
//--- m_featureCacheValid is now always true where m_featureCacheHasValue is true. Both are kept
|
|
//--- rather than collapsed into one array: the pair is written and read in several places, and a
|
|
//--- silent meaning change is exactly how the last version of this drifted.
|
|
if(cacheable && ok)
|
|
{
|
|
m_featureCacheHasValue[idx] = true;
|
|
m_featureCacheValid[idx] = true;
|
|
int base = idx * width;
|
|
int count = TempData.Total() - startTotal;
|
|
for(int f = 0; f < count && f < width; f++)
|
|
m_featureCache[base + f] = TempData.At(startTotal + f);
|
|
}
|
|
return ok;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| THE ONE PLACE a feature WINDOW is assembled. Every consumer - |
|
|
//| training pass 1/2/3, live inference, online learning, the OOS |
|
|
//| continual simulation, the chart rescan and the CPU-inference |
|
|
//| self-check - goes through here, because the thing this function |
|
|
//| fixes is a contract that eight hand-rolled copies of the same |
|
|
//| loop cannot hold on their own. |
|
|
//| |
|
|
//| ORDER IS CHRONOLOGICAL: OLDEST BAR FIRST, bar `r` (the bar being |
|
|
//| predicted) LAST. That is the whole point of this function. |
|
|
//| |
|
|
//| MQL5 timeseries indices run BACKWARDS - index 0 is the newest bar |
|
|
//| and increasing index walks into the past. So the obvious loop, |
|
|
//| `for(b = 0..T-1) BufferTempData(r + b)`, appends the window in |
|
|
//| REVERSE chronological order: the newest bar lands in block 0 and |
|
|
//| the oldest in block T-1. That is what every call site used to do. |
|
|
//| |
|
|
//| For the dense (PAI) and convolutional stacks it is harmless - a |
|
|
//| dense layer learns a weight per position either way, and a conv |
|
|
//| just learns time-mirrored kernels. For the RECURRENT stacks it is |
|
|
//| not, and it is not a subtlety: |
|
|
//| - CNeuronLSTMOCL walks steps t = 0..T-1 reading `inputs + t*Iw` |
|
|
//| (AI\Network.cl, LSTM_SeqStepForward), so step t consumes the |
|
|
//| t-th block in buffer order. |
|
|
//| - Its visible output is the LAST hidden state only - the kernel |
|
|
//| writes `output[id]` solely when `t == steps - 1`. |
|
|
//| - The cell state decays toward the start of the sequence: |
|
|
//| c_t = f*c_{t-1} + i*g. DirectML\lstm_seq_flowcheck.cpp measured |
|
|
//| block 0's influence on the output, relative to block T-1, at |
|
|
//| 1.2e-2 for the shipped LSTM_FORGET_BIAS_INIT of 1.0 (see that |
|
|
//| constant's comment for the full sweep). |
|
|
//| Fed newest-first, that put the bar being PREDICTED at the far end |
|
|
//| of the decay and handed the output to the OLDEST bar in the window |
|
|
//| - roughly 80x backwards, and the exact inverse of what the window |
|
|
//| exists for ("everything known as of this bar's close", see Train() |
|
|
//| 's r comment). Reversing it here makes the final timestep the |
|
|
//| current bar, which is the standard arrangement and the one the |
|
|
//| forget-bias sweep was implicitly reasoning about. |
|
|
//| |
|
|
//| Nothing downstream reads a fixed block position, so this is safe |
|
|
//| for every topology; it re-keys the weight fingerprint (see |
|
|
//| ConfigFingerprint's WIN token) precisely BECAUSE the input vector |
|
|
//| now means something different, and models trained under the old |
|
|
//| order must never load into it. |
|
|
//| |
|
|
//| Returns true only when the COMPLETE, correctly-sized window is in |
|
|
//| TempData - callers must not feedForward on a partial one (a stale |
|
|
//| output layer would be scored against this bar's label). |
|
|
//+------------------------------------------------------------------+
|
|
//+------------------------------------------------------------------+
|
|
//| ONE-SHOT FEATURE-VECTOR AUTOPSY, run the first time pass 1 finds |
|
|
//| usable windows. Samples bars across the whole training range and |
|
|
//| reports any feature slot that is DEGENERATE - constant, mostly |
|
|
//| zero, or non-finite - naming the block it belongs to. |
|
|
//| |
|
|
//| WHY THIS EXISTS. Two separate 2026-08-17 failures were invisible |
|
|
//| for hours because nothing ever looked at the assembled vector: |
|
|
//| |
|
|
//| - a cold ADMovingAverage returned EMPTY_VALUE for every index, |
|
|
//| so every window was rejected and cached as a permanent miss. |
|
|
//| The only symptom was a scan that never finished. |
|
|
//| - the alt-data block 0-filled every bar older than its file's |
|
|
//| first row (2010-01-01), which on USDJPY is ~half the history. |
|
|
//| Training simply absorbed 15 impossible zeros per bar. |
|
|
//| |
|
|
//| Both are the same shape of bug: a block silently produces |
|
|
//| nothing, and every downstream number stays plausible. A model |
|
|
//| CANNOT tell "this feature is always 0" from "this feature is |
|
|
//| genuinely 0 here", and neither can an accuracy figure - so the |
|
|
//| check has to happen where the values are, once, and shout. |
|
|
//| |
|
|
//| Deliberately a REPORT, not a gate: a degenerate feature is |
|
|
//| sometimes legitimate (a flag that is rare in this window), and |
|
|
//| refusing to train would turn a diagnostic into an outage. It |
|
|
//| names the problem and lets the operator decide. |
|
|
//+------------------------------------------------------------------+
|
|
void CExpertSignalAIBase::ReportFeatureHealth(int bars)
|
|
{
|
|
if(m_featureHealthReported || m_neuronsCount <= 0)
|
|
return;
|
|
m_featureHealthReported = true;
|
|
int per = m_neuronsCount; // features per BAR
|
|
int lo = MathMax((int)m_historyBars + MathMax(m_barrierHorizonBars, 1) + 2, 2);
|
|
int hi = MathMax(bars - 2, lo);
|
|
if(hi <= lo)
|
|
return;
|
|
//--- Evenly spaced sample across the whole range, so a block that dies only in the deep history
|
|
//--- (the alt-coverage case) is caught as surely as one that is dead everywhere (the cold-indicator
|
|
//--- case). 400 bars is enough to call a feature constant and costs a fraction of one era.
|
|
int want = 400;
|
|
int step = MathMax((hi - lo) / want, 1);
|
|
double vmin[], vmax[];
|
|
int zeroCnt[], seen = 0;
|
|
ArrayResize(vmin, per);
|
|
ArrayResize(vmax, per);
|
|
ArrayResize(zeroCnt, per);
|
|
for(int j = 0; j < per; j++)
|
|
{
|
|
vmin[j] = DBL_MAX;
|
|
vmax[j] = -DBL_MAX;
|
|
zeroCnt[j] = 0;
|
|
}
|
|
for(int i = lo; i <= hi; i += step)
|
|
{
|
|
//--- Read ONE bar's block, not a whole window: the per-bar row is what the blocks produce, and
|
|
//--- BuildFeatureWindow would just replicate it historyBars times.
|
|
TempData.Clear();
|
|
if(!BufferTempData(i))
|
|
continue;
|
|
if(TempData.Total() < per)
|
|
continue;
|
|
//--- The bar's own row is the LAST `per` values (BufferTempData appends).
|
|
int base = TempData.Total() - per;
|
|
for(int j = 0; j < per; j++)
|
|
{
|
|
double v = TempData.At(base + j);
|
|
if(!MathIsValidNumber(v))
|
|
continue;
|
|
if(v < vmin[j]) vmin[j] = v;
|
|
if(v > vmax[j]) vmax[j] = v;
|
|
if(v == 0.0) zeroCnt[j]++;
|
|
}
|
|
seen++;
|
|
}
|
|
if(seen < 20)
|
|
{
|
|
Print(ID + StringFormat(": feature health - only %d of %d sampled bars produced a readable row;"
|
|
" too few to judge. This is itself a warning: if it persists the feature"
|
|
" path is rejecting nearly everything.", seen, want));
|
|
return;
|
|
}
|
|
//--- The alt block is appended LAST in every row (see BufferTempDataCompute), so its slots are the
|
|
//--- final FeatureCount() of the `per`. Naming it matters: "slot 47 is constant" is a puzzle,
|
|
//--- "the alt-data block is constant" is an answer.
|
|
int altN = (m_useAltData ? m_altData.FeatureCount() : 0);
|
|
int altFrom = (altN > 0 && altN <= per) ? per - altN : -1;
|
|
string deadList = "", zeroList = "";
|
|
int dead = 0, mostlyZero = 0;
|
|
for(int j = 0; j < per; j++)
|
|
{
|
|
if(vmin[j] > vmax[j])
|
|
continue; // never read
|
|
bool isConst = (vmax[j] - vmin[j]) <= 1e-12;
|
|
bool isZeroy = (zeroCnt[j] * 2 > seen);
|
|
string tag = (altFrom >= 0 && j >= altFrom)
|
|
? StringFormat("alt[%d]", j - altFrom) : StringFormat("f%d", j);
|
|
if(isConst)
|
|
{
|
|
dead++;
|
|
if(dead <= 12)
|
|
deadList += StringFormat("%s%s=%.4g", (deadList == "" ? "" : " "), tag, vmin[j]);
|
|
}
|
|
else
|
|
if(isZeroy)
|
|
{
|
|
mostlyZero++;
|
|
if(mostlyZero <= 12)
|
|
zeroList += StringFormat("%s%s(%.0f%%)", (zeroList == "" ? "" : " "), tag,
|
|
100.0 * zeroCnt[j] / seen);
|
|
}
|
|
}
|
|
Print(ID + StringFormat(": FEATURE HEALTH on %d sampled bars x %d features%s - %d CONSTANT%s%s |"
|
|
" %d mostly-zero (>50%%)%s%s. A constant feature contributes nothing but"
|
|
" still consumes a first-layer column and a BatchNorm slot; a block that is"
|
|
" constant AND zero is usually a source that failed silently rather than a"
|
|
" quiet market.",
|
|
seen, per,
|
|
(altFrom >= 0 ? StringFormat(" (alt block = slots %d..%d)", altFrom, per - 1)
|
|
: " (no alt block)"),
|
|
dead, (deadList == "" ? "" : ": "), deadList,
|
|
mostlyZero, (zeroList == "" ? "" : ": "), zeroList));
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::BuildFeatureWindow(int r)
|
|
{
|
|
int width = (int)m_historyBars * m_neuronsCount;
|
|
TempData.Clear();
|
|
TempData.Reserve(width);
|
|
if(r < 0 || m_historyBars <= 0 || m_neuronsCount <= 0)
|
|
return false;
|
|
//--- Live-only freshness probe for the external block: two comparisons when quiet, a reload at
|
|
//--- most hourly once the chart outruns the exported data. Never fires in the tester (the newest
|
|
//--- bar is historical there). Safe against the feature cache: new alt rows only exist for a new
|
|
//--- D1 bar, and a new bar resets the per-bar-frame cache anyway.
|
|
if(m_useAltData)
|
|
m_altData.EnsureFresh((datetime)m_Time.GetData(0));
|
|
//--- b counts bars BACK from r, so (m_historyBars - 1 - b) emits the deepest lookback first and
|
|
//--- lands on r itself on the final iteration. Identical set of bars as before, opposite order.
|
|
for(int b = 0; b < (int)m_historyBars; b++)
|
|
if(!BufferTempData(r + ((int)m_historyBars - 1 - b)))
|
|
{
|
|
//--- Which lookback slot rejected, and how much of the window had been assembled. Without
|
|
//--- this the pass-1 stall report can only say "0 of 54681 usable", which is true of a cold
|
|
//--- ATR, a missing optional block and an out-of-range index alike.
|
|
m_windowFailSlot = b;
|
|
m_windowFailTotal = TempData.Total();
|
|
return false;
|
|
}
|
|
if(TempData.Total() < width)
|
|
{
|
|
//--- Nothing rejected the bar and the window is still short. Distinct fault from a guard
|
|
//--- rejection and it used to be indistinguishable from one; the per-bar width contract in
|
|
//--- BufferTempData should now catch this first, so reaching here means the shortfall is in the
|
|
//--- window assembly itself rather than in one bar's blocks.
|
|
m_windowFailSlot = -1;
|
|
m_windowFailTotal = TempData.Total();
|
|
return false;
|
|
}
|
|
//--- THE ANCHOR BAR'S EXTERNAL READING ENTERS THE WINDOW ONCE, NOT ONCE PER BAR OF ITS DAY (2026-08-16).
|
|
//---
|
|
//--- The alt block is appended LAST in every bar's row (see BufferTempDataCompute), and its lookup
|
|
//--- is as-of by bar open time into a DAILY file - so every bar of the window that falls on the same
|
|
//--- calendar day gets byte-identical values. Measured on the live SP500 D1 export (6073 rows, 13
|
|
//--- features, 5888 windows): each feature takes 1.7-2.7 distinct values across the 16 slots, 12
|
|
//--- principal components carry 95% of the block's variance and its effective rank is ~11.5. So 208
|
|
//--- inputs were carrying about 12 dimensions. Only 6 of the 13 features move daily at all; 5 are
|
|
//--- weekly (COT, EIA) and 2 monthly (CPI, unemployment).
|
|
//---
|
|
//--- The cost is NOT overfitting capacity - the copies are collinear, so they span ~12 directions,
|
|
//--- not 208. It is the GRADIENT WEIGHTING. Batch norm standardizes each of the 208 coordinates
|
|
//--- independently, which rescales the copies but does not decorrelate them, so the same factor
|
|
//--- arrives on 16 unit-variance coordinates. Each of their weights takes a full-size step, and the
|
|
//--- factor's aggregate coefficient therefore moves ~16x faster than a per-bar price feature's. The
|
|
//--- network was structurally biased toward the external block by a factor of the window length -
|
|
//--- an unintended prior, and pointing the wrong way, since these features cleared only a marginal
|
|
//--- incremental screen while price features are the base signal.
|
|
//---
|
|
//--- Zeroed here, at WINDOW ASSEMBLY, rather than in BufferTempData: that function's output is cached
|
|
//--- PER BAR (m_featureCache) and a bar sits at slot 15 of one window and slot 0 of the next, so a
|
|
//--- slot-dependent value there would either poison the cache or force a recompute on every slot.
|
|
//--- The cache keeps the true values; only the copies in this window are cleared. The width contract
|
|
//--- is untouched (same count, same positions) so conv/LSTM/HYBRID keep their bar-major rectangle and
|
|
//--- need no change - and on an intraday chart the block arrives at the newest bar, which for the
|
|
//--- LSTM is the final timestep, where a "current state of the world" reading belongs. Near-constant
|
|
//--- coordinates are safe through batch norm: its divisor is
|
|
//--- MathMax(MathSqrt(var + BN_EPSILON), BN_MIN_STD).
|
|
//---
|
|
//--- ONLY THE ANCHOR'S OWN RUN IS COLLAPSED. Nothing older is touched, ever.
|
|
//---
|
|
//--- The window's newest slot IS the bar being predicted (see the loop above - the final iteration
|
|
//--- lands on r itself, and pass 3 grades that same index), so the reading in that slot is the one
|
|
//--- contemporaneous with the decision, and it is the only one every alt screen actually validated.
|
|
//--- This walks back from the anchor blanking bars that carry a BYTE-IDENTICAL copy of it, and stops
|
|
//--- at the first bar that differs. Because an as-of lookup into a daily file is a step function in
|
|
//--- time, those copies are exactly the contiguous run of bars sharing the anchor's calendar day.
|
|
//---
|
|
//--- WHY IT STOPS THERE rather than deduplicating the whole window (2026-08-16, user's call before
|
|
//--- deploy: "I would rather avoid lagging so the NN finds accurate patterns"). Collapsing every
|
|
//--- distinct reading would leave each surviving at ONE slot - but WHICH slot depends on where the
|
|
//--- day boundary falls inside that particular window, so a given lag would land on a different
|
|
//--- coordinate from one window to the next. A dense layer holds a separate weight per (slot,
|
|
//--- feature), so that turns a stable lagged input into a moving one. The older readings' natural
|
|
//--- replicated runs are therefore left exactly as they were: whatever the net had learned to read
|
|
//--- from those slots, it still reads from those same slots. The alt screens measured the CURRENT
|
|
//--- reading's MI against forward range and never tested lags, so the lagged content is unproven -
|
|
//--- and unproven is a reason to leave it undisturbed, not a licence to rearrange it.
|
|
//---
|
|
//--- What this still buys: the anchor's reading reaches the first layer on ONE coordinate instead of
|
|
//--- once per bar of its day, which removes the gradient upweight described above for the only
|
|
//--- reading that was ever validated. It is also IDENTICAL to full deduplication exactly where the
|
|
//--- replication was worst - on M15 and H1 the entire window sits inside one calendar day, so the
|
|
//--- anchor's run IS the whole window - and a no-op on D1, where the bar immediately older than the
|
|
//--- anchor is already a different day and the loop breaks on its first comparison. The only
|
|
//--- timeframes where the two differ are the middle ones, and there this is the conservative choice.
|
|
if(m_useAltData)
|
|
{
|
|
int an = m_altData.FeatureCount();
|
|
if(an > 0 && an <= m_neuronsCount && (int)m_historyBars > 1)
|
|
{
|
|
double anchor[];
|
|
ArrayResize(anchor, an);
|
|
int newest = ((int)m_historyBars - 1) * m_neuronsCount + (m_neuronsCount - an);
|
|
for(int k = 0; k < an; k++)
|
|
anchor[k] = TempData.At(newest + k);
|
|
for(int b = (int)m_historyBars - 2; b >= 0; b--)
|
|
{
|
|
int altBase = b * m_neuronsCount + (m_neuronsCount - an);
|
|
bool same = true;
|
|
for(int k = 0; k < an && same; k++)
|
|
if(TempData.At(altBase + k) != anchor[k])
|
|
same = false;
|
|
if(!same)
|
|
break; // a different reading: this bar and everything older keep their values as-is
|
|
for(int k = 0; k < an; k++)
|
|
TempData.Update(altBase + k, 0.0);
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| (Re)build the cross-asset panel over `bars` bars. |
|
|
//| |
|
|
//| Called from the same places that size the price buffers, because |
|
|
//| the panel is aligned to exactly that bar grid and a stale panel |
|
|
//| would silently mis-index. Cheap to call redundantly: Build() is |
|
|
//| one CopyClose per reference pair, not per bar. |
|
|
//| |
|
|
//| A failure here is NOT fatal. The panel logs its own reason and |
|
|
//| every Features() call then 0-fills, so the run continues without |
|
|
//| the cross-asset block instead of refusing to train. |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::BuildCrossAssetPanel(int bars)
|
|
{
|
|
if(!m_useCrossAsset)
|
|
return true;
|
|
if(bars <= 0)
|
|
return false;
|
|
//--- Deep enough AND anchored to the current newest bar. Depth alone would leave the panel's
|
|
//--- index 0 pointing at a bar that is no longer the newest as soon as one candle closes, so
|
|
//--- every cross-asset feature would be read one bar out of step with the price features beside
|
|
//--- it - see m_crossAssetAnchor's declaration comment.
|
|
datetime anchor = m_Time.GetData(0);
|
|
if(m_crossAsset.IsReady() && m_crossAsset.Bars() >= bars && m_crossAssetAnchor == anchor && anchor > 0)
|
|
return true;
|
|
//--- A trained model builds from the pair set it was trained on (adopted from the .cfg), never
|
|
//--- from whatever Market Watch holds today - see m_crossAssetPairsPinned.
|
|
if(m_crossAssetPairsPinned != "" && !m_crossAsset.HasPinnedPairs())
|
|
m_crossAsset.SetPinnedPairs(m_crossAssetPairsPinned);
|
|
if(!m_crossAsset.Build(m_symbol.Name(), (ENUM_TIMEFRAMES)m_period, bars))
|
|
{
|
|
m_crossAssetAnchor = 0;
|
|
return false;
|
|
}
|
|
m_crossAssetAnchor = anchor;
|
|
//--- FIRST successful build of a model with no pinned set yet: this pair set is now this model's
|
|
//--- pair set for life. Stamp it and pin it to the .cfg one-shot, exactly like the derived barrier
|
|
//--- pair (see Labels.mqh's m_geometryCfgSaved block) - the .cfg was written at model creation,
|
|
//--- BEFORE the panel could possibly have built, so without this re-save the pin would live only
|
|
//--- in memory and every restart would silently fall back to discovery.
|
|
if(m_crossAssetPairsPinned == "" && m_crossAsset.UsedPairsCsv() != "")
|
|
{
|
|
m_crossAssetPairsPinned = m_crossAsset.UsedPairsCsv();
|
|
m_crossAsset.SetPinnedPairs(m_crossAssetPairsPinned);
|
|
if(!m_crossAssetCfgSaved && m_activeFileName != "")
|
|
{
|
|
m_crossAssetCfgSaved = true;
|
|
if(SaveTopologyConfiguration(m_activeFileName, m_initialNeuronsCount, m_hiddenLayersCount,
|
|
m_neuronsReduction, m_minNeuronsCount, m_optimizationAlgo,
|
|
m_historyBars, m_outputNeuronsCount, m_neuronsCount,
|
|
LEGACY_STUDY_PERIOD_SLOT, m_minTrainYear, m_isInitialized,
|
|
LEGACY_CONVERGE_WR_SLOT, m_fractalPeriods, m_convFilterCount,
|
|
m_lstmHiddenSize, m_activeFileCommon))
|
|
Print(ID + ": cross-asset pair set PINNED to the .cfg - [" + m_crossAssetPairsPinned +
|
|
"]. Restarts and redeploys now build the panel from exactly this set; Market Watch "
|
|
"changes no longer alter what a trained model's features mean.");
|
|
else
|
|
Print(ID + ": WARNING - failed to pin the cross-asset pair set to the .cfg; a restart "
|
|
"will re-discover Market Watch instead of adopting the trained set.");
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Copy the historical spread series onto the current bar grid. |
|
|
//| |
|
|
//| CopySpread is a RANGE call, so this runs once wherever the price |
|
|
//| buffers are sized - never per bar. Values are in POINTS (int); |
|
|
//| the feature block converts with m_symbol.Point(). |
|
|
//| |
|
|
//| Non-fatal: a short or failed copy leaves m_spreadSeriesBars at |
|
|
//| whatever was actually obtained and the feature block 0-fills past |
|
|
//| it, matching the degraded-but-usable convention used by the swing |
|
|
//| and cross-asset blocks. Refusing to train because one auxiliary |
|
|
//| series came up short would be a far worse failure. |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::EnsureSpreadSeries(int bars)
|
|
{
|
|
//--- The meta target's setup descriptor reads spread/ATR at the candidate's fire bar regardless of
|
|
//--- whether spread is enabled as a per-bar WINDOW feature, so the series must exist for it.
|
|
if(!m_useSpreadFeature && !IsMetaTarget())
|
|
return true;
|
|
if(bars <= 0)
|
|
return false;
|
|
//--- Length alone is NOT a sufficient cache key - see m_spreadSeriesAnchor's declaration comment.
|
|
datetime anchor = m_Time.GetData(0);
|
|
if(m_spreadSeriesBars >= bars && m_spreadSeriesAnchor == anchor && anchor > 0)
|
|
return true;
|
|
ArraySetAsSeries(m_spreadSeries, true); // index 0 = newest, matching every other buffer here
|
|
int got = CopySpread(m_symbol.Name(), (ENUM_TIMEFRAMES)m_period, 0, bars, m_spreadSeries);
|
|
if(got <= 0)
|
|
{
|
|
m_spreadSeriesBars = 0;
|
|
m_spreadSeriesAnchor = 0;
|
|
Print(__FUNCTION__ + ": CopySpread returned " + IntegerToString(got) + " for " + m_symbol.Name() +
|
|
" - spread features 0-filled this run.");
|
|
return false;
|
|
}
|
|
m_spreadSeriesBars = got;
|
|
m_spreadSeriesAnchor = anchor;
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::BufferTempDataCompute(int idx)
|
|
{
|
|
//--- Where THIS bar's block starts. The function appends m_neuronsCount values below; remembering
|
|
//--- the offset lets the whole vector be validated in one place at the end instead of at each of
|
|
//--- the ~60 Add() call sites.
|
|
int featureStart = TempData.Total();
|
|
//--- Cleared here, set by the two NOT-READY-YET guards below. See BufferTempData() for what it
|
|
//--- controls: a rejection caused by data that has not arrived yet must not be cached, because the
|
|
//--- cache never re-tries a miss.
|
|
m_featureFailTransient = false;
|
|
//--- Cleared alongside it, and written by every guard below that can return false - see the
|
|
//--- declaration for why a value COUNT was never enough to identify the block.
|
|
m_featureFailBlock = "";
|
|
m_featureFailIdx = idx;
|
|
double open = m_Open.GetData(idx);
|
|
double close = m_Close.GetData(idx);
|
|
double high = m_High.GetData(idx);
|
|
double low = m_Low.GetData(idx);
|
|
MqlDateTime sTime;
|
|
TimeToStruct(m_Time.GetData(idx), sTime);
|
|
if(open == EMPTY_VALUE)
|
|
{
|
|
m_featureFailTransient = true;
|
|
m_featureFailBlock = "price/open (m_Open.GetData == EMPTY_VALUE)";
|
|
return false;
|
|
}
|
|
// ATR-normalize every raw-price-unit feature below instead of feeding e.g. 0.0005 on EURUSD vs.
|
|
// 50.0 on a JPY pair or an index straight into the network - with Adam and hardcoded, scale-
|
|
// sensitive activations (TANH saturates, PRELU's 0.01 leak only means anything relative to the
|
|
// input's own scale), an unnormalized feature either vanishes into rounding noise or dominates
|
|
// the weighted sum depending on which symbol/timeframe happens to be loaded. Dividing by the
|
|
// bar's own ATR expresses every price-based feature as "fraction of typical volatility", which
|
|
// is comparable across symbols/timeframes and centered near zero. No ATR reading yet (e.g. the
|
|
// first few bars of history) means every price feature this bar would be meaningless - reject
|
|
// the bar via the same "return false" convention as the EMPTY_VALUE check above.
|
|
double atr = m_ATR.Main(idx);
|
|
if(atr <= 0.0 || atr == EMPTY_VALUE)
|
|
{
|
|
//--- TRANSIENT BY NATURE, and the reason resumed models could never train. MT5 calculates an
|
|
//--- indicator's buffers asynchronously after the handle is created, so a call made before ATR
|
|
//--- has filled returns 0 for EVERY index, not just the warm-up tail. A FRESH model never saw
|
|
//--- this: it sits through m_warmupPassesRemaining separately-scheduled Train() calls before
|
|
//--- anything touches a feature, which is exactly what those passes are for. A RESUMED model
|
|
//--- skips them - TuneIndicatorsAndTrain drives StartLabelCachePrebuild and the MI report from
|
|
//--- the very first chart event, milliseconds after OnInit - so it read a cold ATR, every bar
|
|
//--- was rejected, and BufferTempData cached all of it as permanent misses. From then on
|
|
//--- BuildFeatureWindow failed on every bar of every era, add_loop never went true, and pass 1
|
|
//--- swept 0->100% forever with nothing in the journal (2026-08-10; deleting the .nnw "fixed"
|
|
//--- it only by turning the model back into a fresh one).
|
|
m_featureFailTransient = true;
|
|
m_featureFailBlock = StringFormat("ATR (m_ATR.Main=%.10g, needs > 0)", atr);
|
|
return false;
|
|
}
|
|
if(!TempData.Add((close - open) / atr) ||
|
|
!TempData.Add((high - open) / atr) ||
|
|
!TempData.Add((low - open) / atr) ||
|
|
// Explicit bullish/bearish flag - (close-open)/atr already encodes direction *and* magnitude
|
|
// together, which asks the network to disentangle "which way" from "how much" out of a single
|
|
// continuous value. Giving direction its own clean +1/-1/0 signal removes that ambiguity.
|
|
!TempData.Add(close > open ? 1.0 : (close < open ? -1.0 : 0.0)))
|
|
{
|
|
return false;
|
|
}
|
|
if(m_useSwingContext)
|
|
{
|
|
// Most recent CONFIRMED swing pivot as of bar idx - "confirmed" meaning at least
|
|
// m_swingConfirmationBars MORE bars have closed after it (see m_swingConfirmationBars' and
|
|
// m_useSwingContext's declaration comments). This is now the ONLY consumer of that embargo: the
|
|
// label side stopped needing it when the target became the triple barrier, whose own lookahead is
|
|
// m_barrierHorizonBars. Skipping this embargo here - e.g. reading
|
|
// m_ADZigZag's raw current buffer value instead - would leak information a live bar at idx
|
|
// could never actually have had yet, since ZigZag's most recent 1-3 legs are still provisional
|
|
// and can be revised as new bars arrive.
|
|
int pivotIdx = -1;
|
|
double pivotPrice = 0.0;
|
|
bool pivotIsLow = false;
|
|
if(!FindConfirmedZigZagPivot(idx + MathMax(m_swingConfirmationBars, 1), pivotIdx, pivotPrice, pivotIsLow))
|
|
{
|
|
// No confirmed pivot within the scan cap (e.g. right at the start of available history) -
|
|
// this is legitimately "no swing context yet", not bad/missing data, so a neutral 0-fill
|
|
// keeps the bar usable rather than rejecting it outright like the ATR/EMPTY_VALUE guards do.
|
|
if(!TempData.Add(0.0) || !TempData.Add(0.0) || !TempData.Add(0.0) || !TempData.Add(0.0) || !TempData.Add(0.0))
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
// Direction of the CURRENT leg: the last confirmed pivot being a bottom means price has been
|
|
// rising away from it (an up-leg) ever since, and vice versa - same +1/-1 convention as the
|
|
// bullish/bearish flag above, just at swing scale instead of single-bar scale.
|
|
double direction = pivotIsLow ? 1.0 : -1.0;
|
|
// How far price has travelled since that pivot, ATR-normalized and signed (+ve above the
|
|
// pivot price, -ve below) - clamped generously since an extended trending leg has no natural
|
|
// ceiling the way a single bar's range does.
|
|
double distSincePivot = MathMax(-10.0, MathMin(10.0, (close - pivotPrice) / atr));
|
|
// Magnitude of the PRIOR completed leg (the pivot immediately before pivotIdx) - a scale
|
|
// reference for "is the current move big or small relative to the last full swing". No
|
|
// additional embargo needed here (see FindConfirmedZigZagPivot()'s declaration comment) -
|
|
// anything at or before an already-confirmed pivot is necessarily even older.
|
|
int priorPivotIdx = -1;
|
|
double priorPivotPrice = 0.0;
|
|
bool priorPivotIsLow = false;
|
|
bool havePrior = FindConfirmedZigZagPivot(pivotIdx + 1, priorPivotIdx, priorPivotPrice, priorPivotIsLow);
|
|
double priorLegMagnitude = havePrior ? MathMax(0.0, MathMin(10.0, MathAbs(pivotPrice - priorPivotPrice) / atr)) : 0.0;
|
|
// Retracement/extension ratio (current distance relative to the prior leg's own size) -
|
|
// Fibonacci-style relative position, often more informative than either raw magnitude alone
|
|
// since it's comparable across both quiet and volatile regimes. 0 when there's no prior leg
|
|
// to compare against yet.
|
|
double retracementRatio = (havePrior && priorLegMagnitude > 0.0001) ?
|
|
MathMax(-5.0, MathMin(5.0, distSincePivot / priorLegMagnitude)) : 0.0;
|
|
// Swing age (bars since the pivot) - a maturity/exhaustion proxy, same +/- style clamp
|
|
// convention as the volume-ratio feature below.
|
|
double barsSincePivot = MathMax(0.0, MathMin(5.0, (double)(pivotIdx - idx) / 100.0));
|
|
if(!TempData.Add(direction) ||
|
|
!TempData.Add(distSincePivot) ||
|
|
!TempData.Add(priorLegMagnitude) ||
|
|
!TempData.Add(retracementRatio) ||
|
|
!TempData.Add(barsSincePivot))
|
|
return false;
|
|
}
|
|
// Recent price-action context (4 features), computed from CLOSED bars at idx or older only -
|
|
// no ZigZag confirmation, so no repainting and NO embargo, and never stale, unlike the five
|
|
// pivot-anchored features above whose confirmed anchor is always >= m_swingConfirmationBars
|
|
// (~100) bars old. Those describe the OLD structure well but say nothing about the recent leg
|
|
// the bar actually sits in - which is exactly what's needed to tell a genuine reversal at a
|
|
// range extreme from a mid-trend bar that merely looks like a bottom/top (the "clustered
|
|
// counter-trend signals" failure mode). These locate the bar within its recent range and
|
|
// trend so the network can learn that a directional call belongs at an extreme of an extended
|
|
// move, not anywhere the local candle shape resembles a pivot. All windows walk toward OLDER
|
|
// bars (increasing index), so nothing here can see the future.
|
|
double hi20 = high, lo20 = low, hi50 = high, lo50 = low;
|
|
double sum20 = close, oldestClose20 = close;
|
|
int cnt20 = 1;
|
|
for(int w = 1; w < 50; w++)
|
|
{
|
|
int j = idx + w;
|
|
double jc = m_Close.GetData(j);
|
|
double jh = m_High.GetData(j);
|
|
double jl = m_Low.GetData(j);
|
|
// ran off the oldest edge of loaded history (out-of-range reads back as 0/EMPTY_VALUE) -
|
|
// use whatever window we gathered so far rather than rejecting the bar; a shorter early-
|
|
// history window is degraded-but-usable, same spirit as the pivot 0-fill above.
|
|
if(jh == EMPTY_VALUE || jh <= 0.0 || jl <= 0.0)
|
|
break;
|
|
if(jh > hi50)
|
|
hi50 = jh;
|
|
if(jl < lo50)
|
|
lo50 = jl;
|
|
if(w < 20)
|
|
{
|
|
if(jh > hi20)
|
|
hi20 = jh;
|
|
if(jl < lo20)
|
|
lo20 = jl;
|
|
sum20 += jc;
|
|
oldestClose20 = jc;
|
|
cnt20++;
|
|
}
|
|
}
|
|
// Donchian position: where close sits inside the recent high/low range, rescaled to [-1,+1]
|
|
// (-1 = at the range low / bottom candidate, +1 = at the range high / top candidate, 0 = mid-
|
|
// range / mid-trend). Two scales - a short 20-bar and a medium 50-bar view - so the network
|
|
// sees both local and swing-scale extremity. 0 (mid) when the range is degenerate.
|
|
double range20 = hi20 - lo20;
|
|
double range50 = hi50 - lo50;
|
|
double donchPos20 = (range20 > 0.0) ? ((close - lo20) / range20 - 0.5) * 2.0 : 0.0;
|
|
double donchPos50 = (range50 > 0.0) ? ((close - lo50) / range50 - 0.5) * 2.0 : 0.0;
|
|
// Net directional displacement over the recent window, ATR-normalized and signed - the
|
|
// prevailing-trend strength/direction the counter-trend clusters were ignoring.
|
|
double recentReturn = MathMax(-10.0, MathMin(10.0, (close - oldestClose20) / atr));
|
|
// Distance from the recent mean (SMA), ATR-normalized - a stretch/exhaustion proxy distinct
|
|
// from the net return (a move can be far from its mean with little net displacement, or vice
|
|
// versa); genuine reversals tend to be over-extended from equilibrium.
|
|
double smaExtension = MathMax(-10.0, MathMin(10.0, (close - sum20 / cnt20) / atr));
|
|
if(!TempData.Add(donchPos20) ||
|
|
!TempData.Add(donchPos50) ||
|
|
!TempData.Add(recentReturn) ||
|
|
!TempData.Add(smaExtension))
|
|
return false;
|
|
}
|
|
if(m_useVolumes)
|
|
{
|
|
// FOUR values, not one. This block used to feed only the bar-over-bar change ratio below.
|
|
// research/test_volume.py measured all four against the barrier label with a block-permutation
|
|
// null (blocks = the barrier horizon, because adjacent labels share almost their whole outcome
|
|
// window and a free shuffle produces a null far too tight): the LEVEL and the two
|
|
// volume-vs-range interactions each carry information the first difference does not, and the
|
|
// level beats the shipped feature outright on 4 of 6 instrument/geometry cells.
|
|
//
|
|
// Read the magnitudes before expecting much: the excess mutual information is ~2e-4 nats
|
|
// against a label entropy near 1.05, i.e. well under a tenth of one percent of the label's
|
|
// uncertainty. This is real and repeatable across instruments, and it is nowhere near an edge.
|
|
// It is worth having because it costs one 50-bar loop, not because it changes the answer.
|
|
double vNow = m_Volumes.Main(idx);
|
|
double prevVolume = m_Volumes.Main(idx + 1);
|
|
double volumeDelta = vNow - prevVolume;
|
|
// Relative change - trading activity magnitude varies wildly across symbols/timeframes, so the
|
|
// previous bar's own volume is the scale reference, same logic as ATR-normalizing price above.
|
|
// Guard against a zero previous-bar volume (e.g. a holiday-thin session) instead of dividing by
|
|
// it. Clamped to +/-5: unlike the ATR-normalized price features this ratio has no natural
|
|
// ceiling (a 1-tick bar followed by a normal one produces a huge outlier).
|
|
double volumeChangeRatio = prevVolume > 0.0 ? volumeDelta / prevVolume : 0.0;
|
|
// Baseline over the trailing 50 bars, walking toward OLDER bars only (increasing index), so
|
|
// nothing here can see the future. Degraded-but-usable at the oldest edge, same convention as
|
|
// the swing-context window above: a short early-history baseline beats rejecting the bar.
|
|
double volSum = vNow;
|
|
int volCnt = 1;
|
|
for(int w = 1; w < 50; w++)
|
|
{
|
|
double jv = m_Volumes.Main(idx + w);
|
|
if(jv <= 0.0)
|
|
break;
|
|
volSum += jv;
|
|
volCnt++;
|
|
}
|
|
double volBase = volSum / volCnt;
|
|
// LEVEL: is this an active bar or a dead one? The change ratio cannot express this at all -
|
|
// two consecutive dead bars and two consecutive frantic ones both read as ~0 change.
|
|
double volLevel = (volBase > 0.0) ? vNow / volBase : 1.0;
|
|
double rangeAtr = (high - low) / atr;
|
|
// ABSORPTION: range delivered per unit of activity. A low value means heavy participation that
|
|
// went nowhere - supply meeting demand - which is a categorically different bar from heavy
|
|
// participation that travelled. The single change ratio conflates the two.
|
|
double absorption = (volLevel > 0.05) ? rangeAtr / volLevel : 0.0;
|
|
// ...and its converse, effort AND result together, which is the continuation reading.
|
|
double volXrange = volLevel * rangeAtr;
|
|
if(!TempData.Add(MathMax(-5.0, MathMin(5.0, volumeChangeRatio))) ||
|
|
!TempData.Add(MathMax(0.0, MathMin(5.0, volLevel))) ||
|
|
!TempData.Add(MathMax(0.0, MathMin(5.0, absorption))) ||
|
|
!TempData.Add(MathMax(0.0, MathMin(5.0, volXrange))))
|
|
return false;
|
|
}
|
|
if(m_useTime)
|
|
{
|
|
// Normalize time (cyclical encoding)
|
|
if(!TempData.Add(sin(2 * M_PI * sTime.hour / 24.0)))
|
|
return false;
|
|
if(!TempData.Add(cos(2 * M_PI * sTime.hour / 24.0)))
|
|
return false;
|
|
if(!TempData.Add(sin(2 * M_PI * sTime.day_of_week / 7.0)))
|
|
return false;
|
|
if(!TempData.Add(cos(2 * M_PI * sTime.day_of_week / 7.0)))
|
|
return false;
|
|
if(!TempData.Add(sin(2 * M_PI * sTime.mon / 12.0)))
|
|
return false;
|
|
if(!TempData.Add(cos(2 * M_PI * sTime.mon / 12.0)))
|
|
return false;
|
|
}
|
|
if(m_useATR)
|
|
{
|
|
// ATR/close (volatility as a fraction of price), not raw ATR - the raw absolute value is
|
|
// itself unnormalized (e.g. ~0.0012 on EURUSD vs. ~1.5 on gold, and drifts over time even on
|
|
// one symbol as its price level changes), which is exactly the kind of scale-dependent
|
|
// feature this whole normalization pass is fixing everywhere else.
|
|
if(!TempData.Add(close != 0.0 ? atr / close : 0.0))
|
|
return false;
|
|
}
|
|
if(m_useMA)
|
|
{
|
|
// Same ATR-normalized distance-from-level convention as the base OHLC-from-open features above,
|
|
// just measured against the MA instead of the bar's own open - lets the network read where
|
|
// price sits relative to the same MA Signals\SignalMA.mqh votes on. Plus the MA's own
|
|
// bar-over-bar change (also ATR-normalized, since the MA lives in price units and ATR is
|
|
// already this codebase's scale reference for that - see m_useMA's declaration comment for why
|
|
// this isn't volume's previous-bar-ratio scheme instead).
|
|
double maNow = m_MA.GetData(0, idx);
|
|
double maPrev = m_MA.GetData(0, idx + 1);
|
|
if(maNow == EMPTY_VALUE || maPrev == EMPTY_VALUE)
|
|
{
|
|
//--- TRANSIENT, for exactly the reason spelled out at the ATR guard above, and this is the
|
|
//--- guard that proved it: 2026-08-17, six fresh instances on USDJPY and XAUUSD swept
|
|
//--- 33,965-50,162 bars and produced ZERO usable windows, over and over, for 40 minutes.
|
|
//--- The stall report named the spot precisely - "lookback slot 0 REJECTED (window had 24 of
|
|
//--- 832 values)" - and 24 is exactly the core block (4 price + 5 swing + 4 range + 4 volume
|
|
//--- + 6 time + 1 ATR), so feature 25, the first value of THIS block, was the wall.
|
|
//--- ADMovingAverage is a CUSTOM indicator: MT5 fills its buffer asynchronously, so before it
|
|
//--- has calculated it returns EMPTY_VALUE for EVERY index, not just the warm-up tail. Without
|
|
//--- this flag every bar of the sweep was cached as a PERMANENT miss, which is the identical
|
|
//--- failure the ATR guard above was fixed for on 2026-08-10 - the fix was simply never
|
|
//--- propagated to the indicator blocks that follow it.
|
|
//--- Worse, it self-sustained: the discarded era immediately re-swept all 50k bars, and six
|
|
//--- instances doing that in a loop on a six-core box starved the very indicator they were
|
|
//--- waiting on. Marking it transient is what lets the cache retry instead of poisoning.
|
|
m_featureFailTransient = true;
|
|
//--- WHICH of the two reads failed, and whether the indicator is empty EVERYWHERE or only here.
|
|
//--- Those are different faults with different fixes and the old report could not separate them:
|
|
//--- newest bar also EMPTY -> the whole buffer is unreadable (cold, or a dead/invalid handle).
|
|
//--- Every anchor in the sweep fails, ok=0, and no amount of re-sweeping helps.
|
|
//--- newest bar READS -> the buffer is fine and only this depth is missing, i.e. a genuine
|
|
//--- history edge. Recent anchors succeed and ok > 0, so a total failure RULES THIS OUT.
|
|
//--- ok=0 across 50,163 anchors on 2026-08-17 therefore meant the first case, which is exactly
|
|
//--- what "the handle is short on deep history" would NOT produce - and that is the reading the
|
|
//--- report has to hand over instead of leaving to be inferred.
|
|
string maNewest = "reads (buffer live; this is a history-edge miss)";
|
|
if(m_MA.GetData(0, 0) == EMPTY_VALUE)
|
|
maNewest = "ALSO EMPTY (whole buffer unreadable - cold or dead handle, NOT a depth shortfall)";
|
|
m_featureFailBlock = StringFormat("MA (ADMovingAverage) - GetData(%d)=%s GetData(%d)=%s,"
|
|
" newest bar %s, BarsCalculated=%d",
|
|
idx, maNow == EMPTY_VALUE ? "EMPTY" : "ok",
|
|
idx + 1, maPrev == EMPTY_VALUE ? "EMPTY" : "ok",
|
|
maNewest, m_MA.BarsCalculated());
|
|
return false;
|
|
}
|
|
if(!TempData.Add((open - maNow) / atr) ||
|
|
!TempData.Add((high - maNow) / atr) ||
|
|
!TempData.Add((low - maNow) / atr) ||
|
|
!TempData.Add((close - maNow) / atr) ||
|
|
!TempData.Add((maNow - maPrev) / atr))
|
|
return false;
|
|
}
|
|
if(m_useRSI)
|
|
{
|
|
// Already a 0-100 oscillator - /100 is the only transform needed to match the rest of the
|
|
// feature vector's scale (see m_useRSI's declaration comment).
|
|
double rsiNow = m_RSI.Main(idx);
|
|
if(rsiNow == EMPTY_VALUE)
|
|
{
|
|
m_featureFailTransient = true; // not-ready, not no-data - see the MA guard above
|
|
m_featureFailBlock = StringFormat("RSI - Main(%d)=EMPTY, newest bar %s, BarsCalculated=%d", idx,
|
|
m_RSI.Main(0) == EMPTY_VALUE ? "ALSO EMPTY" : "reads",
|
|
m_RSI.BarsCalculated());
|
|
return false;
|
|
}
|
|
if(!TempData.Add(rsiNow / 100.0))
|
|
return false;
|
|
}
|
|
if(m_useMACD)
|
|
{
|
|
// Main and signal lines are price-domain differences of two EMAs, so the same ATR normalization
|
|
// every other price-unit feature here uses applies unchanged. The third value is the histogram
|
|
// (main - signal): algebraically derivable from the first two, but handed over explicitly for the
|
|
// same reason the bullish/bearish flag is handed to the network alongside (close-open)/atr - a
|
|
// value the network would otherwise have to learn to subtract is better given directly, and the
|
|
// histogram (momentum ACCELERATION) is the one term nothing else in this vector carries.
|
|
double macdMain = m_MACDFeature.Main(idx);
|
|
double macdSignal = m_MACDFeature.Signal(idx);
|
|
if(macdMain == EMPTY_VALUE || macdSignal == EMPTY_VALUE)
|
|
{
|
|
m_featureFailTransient = true; // not-ready, not no-data - see the MA guard above
|
|
m_featureFailBlock = StringFormat("MACD - Main(%d)=%s Signal(%d)=%s, newest bar %s,"
|
|
" BarsCalculated=%d", idx,
|
|
macdMain == EMPTY_VALUE ? "EMPTY" : "ok", idx,
|
|
macdSignal == EMPTY_VALUE ? "EMPTY" : "ok",
|
|
m_MACDFeature.Main(0) == EMPTY_VALUE ? "ALSO EMPTY" : "reads",
|
|
m_MACDFeature.BarsCalculated());
|
|
return false;
|
|
}
|
|
if(!TempData.Add(macdMain / atr) ||
|
|
!TempData.Add(macdSignal / atr) ||
|
|
!TempData.Add((macdMain - macdSignal) / atr))
|
|
return false;
|
|
}
|
|
if(m_useIchimoku)
|
|
{
|
|
// LOOKAHEAD, the one thing that matters in this block. MT5's iIchimoku does NOT pre-shift its
|
|
// buffers - it stores raw per-bar values and shifts only the DRAWING (Ichimoku.mq5 sets
|
|
// PLOT_SHIFT=+Kijun on the Senkou A/B cloud plot and -Kijun on the Chikou plot). In series
|
|
// indexing that means:
|
|
// - SenkouSpan*(i) is computed FROM bar i and drawn Kijun bars into the FUTURE, so the cloud
|
|
// actually sitting under bar idx is SenkouSpan*(idx + kijun) - built from bar idx+kijun and
|
|
// older, hence strictly past data. Reading SenkouSpan*(idx) as "the cloud here" is the classic
|
|
// Ichimoku backtest bug and would leak Kijun bars of future information into every example.
|
|
// - SenkouSpan*(idx) with NO offset IS legitimate as the PROJECTED cloud - the part of the chart
|
|
// already drawn ahead of the current bar. A live bar at idx genuinely knows it (it is computed
|
|
// from bar idx), which is why it appears below as its own feature rather than being avoided.
|
|
// - ChinkouSpan(i) is just Close(i) drawn at i+Kijun, so the Chikou plotted AT bar idx would be
|
|
// Close(idx - kijun) - a FUTURE bar. It is never read. The lookahead-free statement of the same
|
|
// reading is "how far is this close from the close Kijun bars ago", the last feature below.
|
|
// Signals\SignalIchimoku.mqh's class comment documents the identical convention for the vote side.
|
|
int kijunShift = m_indicatorTuner.ichiKijun;
|
|
double tenkan = m_Ichimoku.TenkanSen(idx);
|
|
double kijun = m_Ichimoku.KijunSen(idx);
|
|
double spanA = m_Ichimoku.SenkouSpanA(idx + kijunShift); // cloud AS PLOTTED AT bar idx
|
|
double spanB = m_Ichimoku.SenkouSpanB(idx + kijunShift);
|
|
double futureSpanA = m_Ichimoku.SenkouSpanA(idx); // cloud projected AHEAD of bar idx
|
|
double futureSpanB = m_Ichimoku.SenkouSpanB(idx);
|
|
double closeLagRef = m_Close.GetData(idx + kijunShift); // Chikou reference, never idx - kijunShift
|
|
if(tenkan == EMPTY_VALUE || kijun == EMPTY_VALUE ||
|
|
spanA == EMPTY_VALUE || spanB == EMPTY_VALUE ||
|
|
futureSpanA == EMPTY_VALUE || futureSpanB == EMPTY_VALUE ||
|
|
closeLagRef == EMPTY_VALUE || closeLagRef <= 0.0)
|
|
{
|
|
m_featureFailTransient = true; // not-ready, not no-data - see the MA guard above
|
|
//--- closeLagRef is called out separately because it is the one term here that reads the CLOSE
|
|
//--- series at idx + kijunShift, so it fails on the oldest kijunShift bars by construction (see
|
|
//--- ResizeBuffers' clamp note) rather than because Ichimoku is unready.
|
|
m_featureFailBlock = StringFormat("Ichimoku - tenkan=%s kijun=%s spanA=%s spanB=%s fA=%s fB=%s"
|
|
" closeLag(idx+%d)=%s, newest bar %s, BarsCalculated=%d",
|
|
tenkan == EMPTY_VALUE ? "EMPTY" : "ok",
|
|
kijun == EMPTY_VALUE ? "EMPTY" : "ok",
|
|
spanA == EMPTY_VALUE ? "EMPTY" : "ok",
|
|
spanB == EMPTY_VALUE ? "EMPTY" : "ok",
|
|
futureSpanA == EMPTY_VALUE ? "EMPTY" : "ok",
|
|
futureSpanB == EMPTY_VALUE ? "EMPTY" : "ok",
|
|
kijunShift,
|
|
(closeLagRef == EMPTY_VALUE || closeLagRef <= 0.0) ? "EMPTY" : "ok",
|
|
m_Ichimoku.TenkanSen(0) == EMPTY_VALUE ? "ALSO EMPTY" : "reads",
|
|
m_Ichimoku.BarsCalculated());
|
|
return false;
|
|
}
|
|
if(!TempData.Add((close - tenkan) / atr) || // distance to the fast line
|
|
!TempData.Add((close - kijun) / atr) || // distance to the equilibrium line
|
|
!TempData.Add((tenkan - kijun) / atr) || // TK spread: sign = cross state, size = conviction
|
|
!TempData.Add((close - spanA) / atr) || // distance to each cloud edge, so the network can
|
|
!TempData.Add((close - spanB) / atr) || // place price above / inside / below the cloud
|
|
!TempData.Add((spanA - spanB) / atr) || // signed cloud thickness here: sign = regime, size = strength
|
|
!TempData.Add((futureSpanA - futureSpanB) / atr) || // same for the projected cloud - the "twist" ahead
|
|
!TempData.Add((close - closeLagRef) / atr)) // Chikou displacement, in its lookahead-free form
|
|
return false;
|
|
}
|
|
if(m_useNews)
|
|
{
|
|
// Event proximity + impact only - see this member's declaration comment and
|
|
// System\NewsRelevance.mqh's ImpactWeightedProximity() for why the forward-looking half
|
|
// (searchForward=true) isn't lookahead bias despite being computed for a historical bar.
|
|
// What is DELIBERATELY not here is actual-vs-forecast surprise. Release TIMES are published
|
|
// in advance and never revised, so reading them for a historical bar is legitimate; released
|
|
// VALUES are neither. MqlCalendarValue.actual_value returns the FINAL figure, and the calendar
|
|
// keeps no as-of-release snapshot (revised_prev_value exists precisely because revisions
|
|
// happen), so a surprise feature computed for a 2019 bar would be built from a number nobody
|
|
// had in 2019. That is the same class of leak that made the RSI/MACD divergence models read
|
|
// +4 sigma in research/test_classic.py until two bars of lookahead were closed - except this
|
|
// one would survive into production and be paid for in real money.
|
|
datetime barTime = m_Time.GetData(idx);
|
|
double newsRecency = ImpactWeightedProximity(m_symbol.Name(), barTime, m_newsFeatureWindowMinutes, false);
|
|
double newsProximity = ImpactWeightedProximity(m_symbol.Name(), barTime, m_newsFeatureWindowMinutes, true);
|
|
if(!TempData.Add(newsRecency) || !TempData.Add(newsProximity))
|
|
return false;
|
|
}
|
|
if(m_useSpreadFeature)
|
|
{
|
|
// TWO values. What this block actually encodes is worth stating precisely, because the raw
|
|
// measurement overstates it.
|
|
//
|
|
// spr/atr measured as the single strongest feature in this codebase (research/test_spread.py):
|
|
// significant on 5 of 8 instrument/geometry cells at 2-4x any volume feature. But the barrier
|
|
// LABEL is computed with the spread charged inside it, so a high-spread bar has its barriers
|
|
// shifted adversely and is mechanically likelier to resolve as a loss - the feature would
|
|
// partly be predicting its own cost model, which is not tradeable. Re-labelling at zero cost
|
|
// and re-measuring the identical feature showed 20-40% of the signal WAS that tautology and
|
|
// the majority was not (XAUUSD kept 97%).
|
|
//
|
|
// What survives is a VOLATILITY-REGIME reading: the spread is near-fixed while ATR is not, so
|
|
// this ratio runs high exactly when realised volatility is below its own ATR estimate - which
|
|
// genuinely predicts whether ATR-scaled barriers get reached at all. Note it is UNSIGNED, like
|
|
// volume: it informs Neutral-vs-directional and can never pick a side.
|
|
double sprRatio = 0.0, sprChange = 0.0;
|
|
if(idx + 1 < m_spreadSeriesBars)
|
|
{
|
|
double sNow = (double)m_spreadSeries[idx] * m_symbol.Point();
|
|
double sPrev = (double)m_spreadSeries[idx + 1] * m_symbol.Point();
|
|
sprRatio = sNow / atr;
|
|
if(sPrev > 0.0)
|
|
sprChange = (sNow - sPrev) / sPrev;
|
|
}
|
|
if(!TempData.Add(MathMax(0.0, MathMin(5.0, sprRatio))) ||
|
|
!TempData.Add(MathMax(-5.0, MathMin(5.0, sprChange))))
|
|
return false;
|
|
}
|
|
if(m_useCrossAsset)
|
|
{
|
|
// What every OTHER instrument was doing at this bar's timestamp - the one feature block here
|
|
// that is not a function of this symbol's own series. See System\CrossAsset.mqh.
|
|
// A panel that failed to build (no Market Watch pairs, unsynchronised history) yields a
|
|
// neutral 0-fill rather than rejecting the bar: the block is additive context, and losing
|
|
// every bar of training because a reference symbol was missing would be a far worse failure
|
|
// than training without the context. Features() reports that by returning false, which is
|
|
// logged once at build time rather than per bar.
|
|
double xa[];
|
|
m_crossAsset.Features(idx, xa);
|
|
for(int k = 0; k < CROSSASSET_FEATURES; k++)
|
|
if(!TempData.Add(xa[k]))
|
|
return false;
|
|
}
|
|
if(m_useADCumulativeDelta)
|
|
{
|
|
//--- COLD IS TRANSIENT, NOT ZERO (2026-08-11). These raw GetData reads have no EMPTY_VALUE
|
|
//--- guard of their own; a not-yet-calculated indicator returns EMPTY_VALUE for EVERY index,
|
|
//--- the sanitize loop at the bottom rewrites that to 0.0, the bar then SUCCEEDS - and
|
|
//--- BufferTempData caches it as a success for the whole bar frame. That is the one path the
|
|
//--- f6150ee only-cache-successes rule cannot see, because it never fails: on a resumed model
|
|
//--- the era-0 prebuild/MI report start milliseconds after OnInit and could train on all-zero
|
|
//--- Wyckoff/AD blocks for up to a full bar (the ba13eef failure class, arriving through
|
|
//--- values that never fail). ADIndicatorCold probes the NEWEST bar: EMPTY_VALUE there means
|
|
//--- the async calculation hasn't filled yet -> transient reject (never cached, retried like
|
|
//--- the cold-ATR guard above). A warm indicator whose DEEP bars read EMPTY_VALUE (beyond its
|
|
//--- buffer depth) is different - that stays the sanitize loop's neutral-fill, since rejecting
|
|
//--- those bars would starve training of legitimately degraded history.
|
|
if(ADIndicatorCold(m_ADCumulativeDelta, "ADCumulativeDelta"))
|
|
return false;
|
|
// buffers: 0=Pressure, 1=CumulativeDelta, 2=BullishPressure, 3=BearishPressure, 4=Absorption, 5=Initiative.
|
|
// CumulativeDelta (buffer 1) is now cumulativeDelta/sumVolume clamped +/-2 (same scale as every
|
|
// other buffer here, see ADCumulativeDelta.mq5) - a pure order-flow-imbalance ratio, distinct from
|
|
// Pressure (buffer 0), which is this same term further adjusted by Initiative/Absorption.
|
|
if(!TempData.Add(m_ADCumulativeDelta.GetData(0, idx)) || // Pressure
|
|
!TempData.Add(m_ADCumulativeDelta.GetData(1, idx)) || // CumulativeDelta
|
|
!TempData.Add(m_ADCumulativeDelta.GetData(2, idx)) || // BullishPressure
|
|
!TempData.Add(m_ADCumulativeDelta.GetData(3, idx)) || // BearishPressure
|
|
!TempData.Add(m_ADCumulativeDelta.GetData(4, idx)) || // Absorption
|
|
!TempData.Add(m_ADCumulativeDelta.GetData(5, idx))) // Initiative
|
|
return false;
|
|
}
|
|
if(m_useADShorteningOfThrust)
|
|
{
|
|
if(ADIndicatorCold(m_ADShorteningOfThrust, "ADShorteningOfThrust")) // see the CumulativeDelta block's comment
|
|
return false;
|
|
// buffers: 0=SOT, 1=SOTEffortRegime, 2=SOTConfirmation, 3=SOTPushRegime
|
|
if(!TempData.Add(m_ADShorteningOfThrust.GetData(0, idx)) || // SOT
|
|
!TempData.Add(m_ADShorteningOfThrust.GetData(1, idx)) || // SOTEffortRegime
|
|
!TempData.Add(m_ADShorteningOfThrust.GetData(2, idx)) || // SOTConfirmation
|
|
!TempData.Add(m_ADShorteningOfThrust.GetData(3, idx))) // SOTPushRegime
|
|
return false;
|
|
}
|
|
if(m_useADWyckoffEventStream)
|
|
{
|
|
// buffers: 0=EventCode, 1=EventPhase, 2=ZoneTop, 3=ZoneBottom, 4=EventPrice, 5=StructuralPhase,
|
|
// 6=CHoCHTrendToRange, 7=CHoCHRangeToTrend, 8=SlopeAccumulationBullish, 9=SlopeAccumulationBearish,
|
|
// 10=SlopeDistributionBullish, 11=SlopeDistributionBearish, 12=Reaccumulation, 13=Redistribution.
|
|
// Buffer 4 (EventPrice) is deliberately skipped below - per the indicator's own source
|
|
// (ADWyckoffEventStream.mq5: "BufColor[wi]=(ev!=0)?C[i]:0;"), it's just this bar's close price
|
|
// echoed back when an event fires (0 otherwise), kept only so a charting/backtesting tool like
|
|
// StrategyQuant can anchor an arrow to a price. It carries no information the network doesn't
|
|
// already have (EventCode already flags whether an event fired; the close is already in the
|
|
// base OHLC features), and normalizing it as a "distance from close" like ZoneTop/ZoneBottom
|
|
// below would be actively wrong: it's close-close=0 on event bars but 0-close=-close (a raw,
|
|
// ATR-blown-up price) on every other bar - a huge, meaningless outlier feature.
|
|
// Buffer 1 (EventPhase) USED to be skipped for the same kind of reason - it was written as
|
|
// "BufPhase[wi]=(double)ev;", a byte-for-byte copy of EventCode. The 2026-08-02 rewrite made it a
|
|
// real reading: "BufPhase[wi]=(double)(phaseNow*((dirNow>=0)?1:-1))", i.e. the live range's own
|
|
// Wyckoff phase 1..5 signed by whether that range is accumulation (+) or distribution (-). That is
|
|
// NOT what StructuralPhase (buffer 5) carries: StructuralPhase is derived from the EVENT on this
|
|
// bar (MapStructuralPhase(ev)) and so is 0 on every bar where nothing fires, while EventPhase
|
|
// persists for the whole life of the range. The pair gives the network both "an event just put us
|
|
// in phase C" and "we are still in phase C" - so it is included below.
|
|
// SIGN AND MAGNITUDE SPLIT (2026-08-09 audit, N1). All three of these buffers are signed
|
|
// categoricals of the form (stage * direction), packed into one scalar:
|
|
// EventCode +-1..7 sign = accumulation/distribution, |v| = the Wyckoff schematic stage
|
|
// (1 PS, 2 SC, 3 AR, 4 ST, 5 Spring/UTAD, 6 LPS/LPSY, 7 SOS/SOW)
|
|
// EventPhase +-1..5 the LIVE range's phase, same sign convention, persists between events
|
|
// StructuralPhase +-1..5 this bar's event mapped to a phase, 0 when nothing fired
|
|
// Fed raw, each one asks the network to disentangle "which way" from "how far through the
|
|
// schematic" out of a single continuous value - and to do it across a sign change, where the
|
|
// ordinal jumps from -1 to +1 with nothing in between. That is the exact ambiguity the base OHLC
|
|
// block calls out and fixes by handing direction its own +1/-1/0 flag beside (close-open)/atr;
|
|
// these are the same shape of value and get the same treatment.
|
|
// Information-preserving: (dir, mag) reconstructs the original exactly, so this is a re-encoding
|
|
// and not a feature change. Magnitudes are scaled onto [0,1] by their own maxima so they sit in
|
|
// the same range as the rest of the vector instead of reaching 7.
|
|
// The magnitudes ARE meaningfully ordinal - the schematic is a sequence, not a set of unrelated
|
|
// labels - which is why they stay scalars rather than being one-hot expanded across 7 inputs.
|
|
if(ADIndicatorCold(m_ADWyckoffEventStream, "ADWyckoffEventStream")) // see the CumulativeDelta block's comment
|
|
return false;
|
|
double wesEvent = m_ADWyckoffEventStream.GetData(0, idx);
|
|
double wesLivePhase = m_ADWyckoffEventStream.GetData(1, idx);
|
|
double wesStructPhase = m_ADWyckoffEventStream.GetData(5, idx);
|
|
if(!TempData.Add(wesEvent > 0 ? 1.0 : (wesEvent < 0 ? -1.0 : 0.0)) || // event direction
|
|
!TempData.Add(MathMin(1.0, MathAbs(wesEvent) / 7.0)) || // event stage
|
|
!TempData.Add(wesLivePhase > 0 ? 1.0 : (wesLivePhase < 0 ? -1.0 : 0.0)) || // live-range direction
|
|
!TempData.Add(MathMin(1.0, MathAbs(wesLivePhase) / 5.0)) || // live-range phase
|
|
!TempData.Add((m_ADWyckoffEventStream.GetData(2, idx) - close) / atr) || // ZoneTop
|
|
!TempData.Add((m_ADWyckoffEventStream.GetData(3, idx) - close) / atr) || // ZoneBottom
|
|
!TempData.Add(wesStructPhase > 0 ? 1.0 : (wesStructPhase < 0 ? -1.0 : 0.0)) || // struct direction
|
|
!TempData.Add(MathMin(1.0, MathAbs(wesStructPhase) / 5.0)) || // struct phase
|
|
!TempData.Add(m_ADWyckoffEventStream.GetData(6, idx)) || // CHoCHTrendToRange
|
|
!TempData.Add(m_ADWyckoffEventStream.GetData(7, idx)) || // CHoCHRangeToTrend
|
|
!TempData.Add(m_ADWyckoffEventStream.GetData(8, idx)) || // SlopeAccumulationBullish
|
|
!TempData.Add(m_ADWyckoffEventStream.GetData(9, idx)) || // SlopeAccumulationBearish
|
|
!TempData.Add(m_ADWyckoffEventStream.GetData(10, idx)) || // SlopeDistributionBullish
|
|
!TempData.Add(m_ADWyckoffEventStream.GetData(11, idx)) || // SlopeDistributionBearish
|
|
!TempData.Add(m_ADWyckoffEventStream.GetData(12, idx)) || // Reaccumulation
|
|
!TempData.Add(m_ADWyckoffEventStream.GetData(13, idx))) // Redistribution
|
|
return false;
|
|
}
|
|
if(m_useADWyckoffFailedStructure)
|
|
{
|
|
if(ADIndicatorCold(m_ADWyckoffFailedStructure, "ADWyckoffFailedStructure")) // see the CumulativeDelta block's comment
|
|
return false;
|
|
// buffers: 0=Value, 1=BullishStructuralFailure, 2=BearishStructuralFailure, 3=FailedAccumulation, 4=FailedDistribution
|
|
if(!TempData.Add(m_ADWyckoffFailedStructure.GetData(0, idx)) || // Value
|
|
!TempData.Add(m_ADWyckoffFailedStructure.GetData(1, idx)) || // BullishStructuralFailure
|
|
!TempData.Add(m_ADWyckoffFailedStructure.GetData(2, idx)) || // BearishStructuralFailure
|
|
!TempData.Add(m_ADWyckoffFailedStructure.GetData(3, idx)) || // FailedAccumulation
|
|
!TempData.Add(m_ADWyckoffFailedStructure.GetData(4, idx))) // FailedDistribution
|
|
return false;
|
|
}
|
|
if(m_useADWyckoffSignificantBarInversion)
|
|
{
|
|
if(ADIndicatorCold(m_ADWyckoffSignificantBarInversion, "ADWyckoffSignificantBarInversion")) // see the CumulativeDelta block's comment
|
|
return false;
|
|
// buffers: 0=SignificantBarQuality, 1=BullishSignificantBar, 2=BearishSignificantBar, 3=BullishControlFlip, 4=BearishControlFlip
|
|
if(!TempData.Add(m_ADWyckoffSignificantBarInversion.GetData(0, idx)) || // SignificantBarQuality
|
|
!TempData.Add(m_ADWyckoffSignificantBarInversion.GetData(1, idx)) || // BullishSignificantBar
|
|
!TempData.Add(m_ADWyckoffSignificantBarInversion.GetData(2, idx)) || // BearishSignificantBar
|
|
!TempData.Add(m_ADWyckoffSignificantBarInversion.GetData(3, idx)) || // BullishControlFlip
|
|
!TempData.Add(m_ADWyckoffSignificantBarInversion.GetData(4, idx))) // BearishControlFlip
|
|
return false;
|
|
}
|
|
if(m_useAltData)
|
|
{
|
|
//--- External publication-stamped block (COT/VIX/macro) - see System\AltData.mqh and the
|
|
//--- matching m_neuronsCount block in Topology.mqh. As-of lookup by THIS bar's open time, so
|
|
//--- a bar can only read values the live run would have had. Gaps 0-fill like cross-asset:
|
|
//--- the block is additive context and "no reading yet" (pre-history warm-up, collector
|
|
//--- outage, pinned column gone from the file) must degrade, never reject the bar. m_Time is
|
|
//--- unconditional (see InitTime), so GetData here is safe on every path that reaches this.
|
|
double av[];
|
|
m_altData.Features((datetime)m_Time.GetData(idx), av);
|
|
int an = m_altData.FeatureCount();
|
|
for(int k = 0; k < an; k++)
|
|
if(!TempData.Add(av[k]))
|
|
return false;
|
|
}
|
|
//--- ONE finiteness/plausibility gate for the whole bar, rather than 60-odd individually guarded
|
|
//--- Add() calls. Most blocks above already clamp their own output; the AD/Wyckoff blocks
|
|
//--- deliberately do not, because those indicators emit plain readings with no natural range. But
|
|
//--- MQL5's CDoubleBuffer::At() returns EMPTY_VALUE (DBL_MAX) for any index it holds no data for,
|
|
//--- and (EMPTY_VALUE - close) / atr is ~1e307: still FINITE, so it sails through every downstream
|
|
//--- isfinite() check, and still large enough to overflow the first batch-norm layer's running
|
|
//--- variance and latch that layer to NaN permanently (see NormalizeHost in AI\NeuronBatchNorm.mqh -
|
|
//--- that is the 2026-08-02 "BufferWrite failed for buffer 3" run). Neutral-fill rather than reject
|
|
//--- the bar: "this indicator has no reading here" is the degraded-but-usable case that the swing,
|
|
//--- cross-asset and spread blocks above all already handle the same way.
|
|
int featureEnd = TempData.Total();
|
|
for(int f = featureStart; f < featureEnd; f++)
|
|
{
|
|
double v = TempData.At(f);
|
|
if((!MathIsValidNumber(v) || MathAbs(v) > FEATURE_ABS_MAX) && !TempData.Update(f, 0.0))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| "Is this AD indicator still calculating?" MT5 fills custom- |
|
|
//| indicator buffers asynchronously after the handle is created, and |
|
|
//| a cold one returns EMPTY_VALUE for EVERY index - including the |
|
|
//| newest bar, which a warm indicator always has. So probing buffer |
|
|
//| 0 at index 0 cleanly separates "async calc hasn't run yet" (cold: |
|
|
//| transient reject, retried next call like the cold-ATR guard) from |
|
|
//| "this deep bar is beyond the buffered depth" (warm: neutral-fill |
|
|
//| by the sanitize loop, since that history is degraded-but-usable). |
|
|
//| Cost is one array read per block per bar. (2026-08-11) |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::ADIndicatorCold(CiCustom &ind, string block)
|
|
{
|
|
if(ind.GetData(0, 0) != EMPTY_VALUE)
|
|
return false;
|
|
m_featureFailTransient = true;
|
|
m_featureFailBlock = StringFormat("%s - COLD (newest bar EMPTY, whole buffer unreadable),"
|
|
" BarsCalculated=%d", block, ind.BarsCalculated());
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Initialize Open indicators. |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::InitOpen(CIndicators * indicators)
|
|
{
|
|
//--- check pointer
|
|
if(indicators == NULL)
|
|
return (false);
|
|
//--- add object to collection
|
|
if(!indicators.Add(GetPointer(m_Open)))
|
|
{
|
|
printf(__FUNCTION__ + ": error adding object");
|
|
return (false);
|
|
}
|
|
//--- initialize object
|
|
if(!m_Open.Create(m_symbol.Name(), m_period))
|
|
{
|
|
printf(__FUNCTION__ + ": error initializing object");
|
|
return (false);
|
|
}
|
|
//--- ok
|
|
return (true);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Initialize Close indicators. |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::InitClose(CIndicators * indicators)
|
|
{
|
|
//--- check pointer
|
|
if(indicators == NULL)
|
|
return (false);
|
|
//--- add object to collection
|
|
if(!indicators.Add(GetPointer(m_Close)))
|
|
{
|
|
printf(__FUNCTION__ + ": error adding object");
|
|
return (false);
|
|
}
|
|
//--- initialize object
|
|
if(!m_Close.Create(m_symbol.Name(), m_period))
|
|
{
|
|
printf(__FUNCTION__ + ": error initializing object");
|
|
return (false);
|
|
}
|
|
//--- ok
|
|
return (true);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Initialize High indicators. |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::InitHigh(CIndicators * indicators)
|
|
{
|
|
//--- check pointer
|
|
if(indicators == NULL)
|
|
return (false);
|
|
//--- add object to collection
|
|
if(!indicators.Add(GetPointer(m_High)))
|
|
{
|
|
printf(__FUNCTION__ + ": error adding object");
|
|
return (false);
|
|
}
|
|
//--- initialize object
|
|
if(!m_High.Create(m_symbol.Name(), m_period))
|
|
{
|
|
printf(__FUNCTION__ + ": error initializing object");
|
|
return (false);
|
|
}
|
|
//--- ok
|
|
return (true);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Initialize Low indicators. |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::InitLow(CIndicators * indicators)
|
|
{
|
|
//--- check pointer
|
|
if(indicators == NULL)
|
|
return (false);
|
|
//--- add object to collection
|
|
if(!indicators.Add(GetPointer(m_Low)))
|
|
{
|
|
printf(__FUNCTION__ + ": error adding object");
|
|
return (false);
|
|
}
|
|
//--- initialize object
|
|
if(!m_Low.Create(m_symbol.Name(), m_period))
|
|
{
|
|
printf(__FUNCTION__ + ": error initializing object");
|
|
return (false);
|
|
}
|
|
//--- ok
|
|
return (true);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Initialize Time indicators. |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::InitTime(CIndicators * indicators)
|
|
{
|
|
//--- check pointer
|
|
if(indicators == NULL)
|
|
return (false);
|
|
//--- add object to collection
|
|
if(!indicators.Add(GetPointer(m_Time)))
|
|
{
|
|
printf(__FUNCTION__ + ": error adding object");
|
|
return (false);
|
|
}
|
|
//--- initialize object
|
|
if(!m_Time.Create(m_symbol.Name(), m_period))
|
|
{
|
|
printf(__FUNCTION__ + ": error initializing object");
|
|
return (false);
|
|
}
|
|
//--- ok
|
|
return (true);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Initialize Volumes indicators. |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::InitVolumes(CIndicators * indicators)
|
|
{
|
|
//--- check pointer
|
|
if(indicators == NULL)
|
|
return (false);
|
|
//--- add object to collection
|
|
if(!indicators.Add(GetPointer(m_Volumes)))
|
|
{
|
|
printf(__FUNCTION__ + ": error adding object");
|
|
return (false);
|
|
}
|
|
//--- initialize object
|
|
if(!m_Volumes.Create(m_symbol.Name(), m_period, VolumeData))
|
|
{
|
|
printf(__FUNCTION__ + ": error initializing object");
|
|
return (false);
|
|
}
|
|
//--- ok
|
|
return (true);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Initialize MA indicator (feature use - see m_useMA). Period comes |
|
|
//| from m_indicatorTuner.maPeriod, not the raw PeriodMA input - it |
|
|
//| starts equal to it (see CADIndicatorTuner's constructor) but may |
|
|
//| diverge once AutoTuneIndicators actually searches a trial. The |
|
|
//| Classic Signals MA vote is unaffected - see m_useMA's declaration |
|
|
//| comment. |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::InitMA(CIndicators * indicators, bool addToCollection)
|
|
{
|
|
if(indicators == NULL)
|
|
return (false);
|
|
if(addToCollection && !indicators.Add(GetPointer(m_MA)))
|
|
{
|
|
printf(__FUNCTION__ + ": error adding object");
|
|
return (false);
|
|
}
|
|
//--- unified MA custom indicator (CustomIndicators\ADMovingAverage.mq5); type AND period are both tuner-
|
|
//--- driven (m_indicatorTuner.maType/maPeriod). params[1..] mirror the indicator's own input order.
|
|
MqlParam params[9];
|
|
params[0].type = TYPE_STRING; params[0].string_value = WARRIOR_CI("ADMovingAverage");
|
|
params[1].type = TYPE_INT; params[1].integer_value = m_indicatorTuner.maType; // InpType
|
|
params[2].type = TYPE_INT; params[2].integer_value = m_indicatorTuner.maPeriod; // InpPeriod
|
|
params[3].type = TYPE_INT; params[3].integer_value = PRICE_CLOSE; // InpAppliedPrice
|
|
params[4].type = TYPE_DOUBLE; params[4].double_value = 0.85; // InpOffset (ALMA)
|
|
params[5].type = TYPE_DOUBLE; params[5].double_value = 6.0; // InpSigma (ALMA)
|
|
params[6].type = TYPE_DOUBLE; params[6].double_value = 0.7; // InpVolumeFactor (T3)
|
|
params[7].type = TYPE_DOUBLE; params[7].double_value = 0.001; // InpProcessNoise (Kalman)
|
|
params[8].type = TYPE_DOUBLE; params[8].double_value = 0.1; // InpMeasurementNoise (Kalman)
|
|
if(!m_MA.Create(m_symbol.Name(), m_period, IND_CUSTOM, 9, params))
|
|
{
|
|
printf(__FUNCTION__ + ": error initializing object");
|
|
return (false);
|
|
}
|
|
m_MA.NumBuffers(1);
|
|
return (true);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Initialize RSI indicator (feature use - see m_useRSI). Period |
|
|
//| comes from m_indicatorTuner.rsiPeriod - see InitMA()'s comment. |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::InitRSI(CIndicators * indicators, bool addToCollection)
|
|
{
|
|
if(indicators == NULL)
|
|
return (false);
|
|
if(addToCollection && !indicators.Add(GetPointer(m_RSI)))
|
|
{
|
|
printf(__FUNCTION__ + ": error adding object");
|
|
return (false);
|
|
}
|
|
if(!m_RSI.Create(m_symbol.Name(), m_period, m_indicatorTuner.rsiPeriod, PRICE_CLOSE))
|
|
{
|
|
printf(__FUNCTION__ + ": error initializing object");
|
|
return (false);
|
|
}
|
|
return (true);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Initialize MACD indicator (feature use - see m_useMACD). Periods |
|
|
//| come from m_indicatorTuner.macdFast/macdSlow/macdSignal - see |
|
|
//| InitMA()'s comment for the "starts at the input, may diverge once |
|
|
//| the tuner searches" split, and note the Classic Signals MACD vote |
|
|
//| (Signals\SignalMACD.mqh) keeps its own separate instance. |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::InitMACDFeature(CIndicators * indicators, bool addToCollection)
|
|
{
|
|
if(indicators == NULL)
|
|
return (false);
|
|
if(addToCollection && !indicators.Add(GetPointer(m_MACDFeature)))
|
|
{
|
|
printf(__FUNCTION__ + ": error adding object");
|
|
return (false);
|
|
}
|
|
if(!m_MACDFeature.Create(m_symbol.Name(), m_period, m_indicatorTuner.macdFast, m_indicatorTuner.macdSlow,
|
|
m_indicatorTuner.macdSignal, PRICE_CLOSE))
|
|
{
|
|
printf(__FUNCTION__ + ": error initializing object");
|
|
return (false);
|
|
}
|
|
return (true);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Initialize Ichimoku indicator (feature use - see m_useIchimoku). |
|
|
//| Periods come from m_indicatorTuner.ichiTenkan/ichiKijun/ |
|
|
//| ichiSenkou - see InitMA()'s comment. The Classic Signals Ichimoku |
|
|
//| vote (Signals\SignalIchimoku.mqh) keeps its own instance. |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::InitIchimoku(CIndicators * indicators, bool addToCollection)
|
|
{
|
|
if(indicators == NULL)
|
|
return (false);
|
|
if(addToCollection && !indicators.Add(GetPointer(m_Ichimoku)))
|
|
{
|
|
printf(__FUNCTION__ + ": error adding object");
|
|
return (false);
|
|
}
|
|
if(!m_Ichimoku.Create(m_symbol.Name(), m_period, m_indicatorTuner.ichiTenkan, m_indicatorTuner.ichiKijun,
|
|
m_indicatorTuner.ichiSenkou))
|
|
{
|
|
printf(__FUNCTION__ + ": error initializing object");
|
|
return (false);
|
|
}
|
|
return (true);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Initialize AD Cumulative Delta (CustomIndicators\ADCumulativeDelta.mq5) |
|
|
//| Loaded via iCustom/CiCustom, not a built-in Ci* class - the compiled |
|
|
//| indicator must be present under MQL5\Indicators\ (see |
|
|
//| ExtractCustomIndicators() in Warrior_EA.mq5). Uses the indicator's |
|
|
//| own input defaults; 6 output buffers, one TempData feature each. |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::InitADCumulativeDelta(CIndicators * indicators, bool addToCollection)
|
|
{
|
|
//--- check pointer
|
|
if(indicators == NULL)
|
|
return (false);
|
|
//--- add object to collection
|
|
if(addToCollection && !indicators.Add(GetPointer(m_ADCumulativeDelta)))
|
|
{
|
|
printf(__FUNCTION__ + ": error adding object");
|
|
return (false);
|
|
}
|
|
//--- initialize object; params[1..] mirror ADCumulativeDelta.mq5's own input order exactly
|
|
MqlParam params[11];
|
|
params[0].type = TYPE_STRING;
|
|
params[0].string_value = WARRIOR_CI("ADCumulativeDelta");
|
|
params[1].type = TYPE_INT;
|
|
params[1].integer_value = m_indicatorTuner.adCumDelta.lookback; // InpLookbackPeriod
|
|
params[2].type = TYPE_DOUBLE;
|
|
params[2].double_value = m_indicatorTuner.adCumDelta.volClimax; // InpVolumeClimaxMultiplier
|
|
params[3].type = TYPE_DOUBLE;
|
|
params[3].double_value = m_indicatorTuner.adCumDelta.volHigh; // InpVolumeHighMultiplier
|
|
params[4].type = TYPE_DOUBLE;
|
|
params[4].double_value = m_indicatorTuner.adCumDelta.rangeClimax; // InpRangeClimaxMultiplier
|
|
params[5].type = TYPE_DOUBLE;
|
|
params[5].double_value = m_indicatorTuner.adCumDelta.rangeSignificant; // InpRangeSignificantMult
|
|
params[6].type = TYPE_DOUBLE;
|
|
params[6].double_value = m_indicatorTuner.adCumDelta.stVolRatio; // InpSTVolumeRatio
|
|
params[7].type = TYPE_DOUBLE;
|
|
params[7].double_value = m_indicatorTuner.adCumDelta.atrMult; // InpATRMultiplier
|
|
params[8].type = TYPE_INT;
|
|
params[8].integer_value = 0; // InpContextMode - DO NOT tune
|
|
params[9].type = TYPE_INT;
|
|
params[9].integer_value = 5; // InpSessionType - DO NOT tune
|
|
params[10].type = TYPE_INT;
|
|
params[10].integer_value = 1; // InpSessionCount - DO NOT tune
|
|
if(!m_ADCumulativeDelta.Create(m_symbol.Name(), m_period, IND_CUSTOM, 11, params))
|
|
{
|
|
printf(__FUNCTION__ + ": error initializing object");
|
|
return (false);
|
|
}
|
|
m_ADCumulativeDelta.NumBuffers(6);
|
|
//--- ok
|
|
return (true);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Initialize AD Shortening of Thrust (CustomIndicators\ADShorteningOfThrust.mq5) |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::InitADShorteningOfThrust(CIndicators * indicators, bool addToCollection)
|
|
{
|
|
//--- check pointer
|
|
if(indicators == NULL)
|
|
return (false);
|
|
//--- add object to collection
|
|
if(addToCollection && !indicators.Add(GetPointer(m_ADShorteningOfThrust)))
|
|
{
|
|
printf(__FUNCTION__ + ": error adding object");
|
|
return (false);
|
|
}
|
|
//--- initialize object; params[1..] mirror ADShorteningOfThrust.mq5's own input order exactly
|
|
MqlParam params[7];
|
|
params[0].type = TYPE_STRING;
|
|
params[0].string_value = WARRIOR_CI("ADShorteningOfThrust");
|
|
params[1].type = TYPE_INT;
|
|
params[1].integer_value = m_indicatorTuner.adSOT.thrustLookback; // InpThrustLookback
|
|
params[2].type = TYPE_INT;
|
|
params[2].integer_value = m_indicatorTuner.adSOT.minImpulses; // InpMinImpulses
|
|
params[3].type = TYPE_DOUBLE;
|
|
params[3].double_value = m_indicatorTuner.adSOT.sotThreshold; // InpSOTThreshold
|
|
params[4].type = TYPE_INT;
|
|
params[4].integer_value = 0; // InpContextMode - DO NOT tune
|
|
params[5].type = TYPE_INT;
|
|
params[5].integer_value = 5; // InpSessionType - DO NOT tune
|
|
params[6].type = TYPE_INT;
|
|
params[6].integer_value = 1; // InpSessionCount - DO NOT tune
|
|
if(!m_ADShorteningOfThrust.Create(m_symbol.Name(), m_period, IND_CUSTOM, 7, params))
|
|
{
|
|
printf(__FUNCTION__ + ": error initializing object");
|
|
return (false);
|
|
}
|
|
m_ADShorteningOfThrust.NumBuffers(4);
|
|
//--- ok
|
|
return (true);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Initialize AD Wyckoff Event Stream (CustomIndicators\ADWyckoffEventStream.mq5) |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::InitADWyckoffEventStream(CIndicators * indicators, bool addToCollection)
|
|
{
|
|
//--- check pointer
|
|
if(indicators == NULL)
|
|
return (false);
|
|
//--- add object to collection
|
|
if(addToCollection && !indicators.Add(GetPointer(m_ADWyckoffEventStream)))
|
|
{
|
|
printf(__FUNCTION__ + ": error adding object");
|
|
return (false);
|
|
}
|
|
//--- initialize object; params[1..] mirror ADWyckoffEventStream.mq5's own input order exactly.
|
|
//--- NOTE the order is NOT grouped by meaning: the three range-lifecycle knobs the indicator gained on
|
|
//--- 2026-08-02 were appended AFTER the session inputs, not next to the thresholds they belong with.
|
|
//--- MqlParam is positional, so this list follows the indicator's declaration order, not a tidier one.
|
|
MqlParam params[17];
|
|
params[0].type = TYPE_STRING;
|
|
params[0].string_value = WARRIOR_CI("ADWyckoffEventStream");
|
|
params[1].type = TYPE_INT;
|
|
params[1].integer_value = m_indicatorTuner.adWES.lookback; // InpLookback
|
|
params[2].type = TYPE_INT;
|
|
params[2].integer_value = m_indicatorTuner.adWES.zigzag; // InpZigZag
|
|
params[3].type = TYPE_DOUBLE;
|
|
params[3].double_value = m_indicatorTuner.adWES.volClimax; // InpVolClimax
|
|
params[4].type = TYPE_DOUBLE;
|
|
params[4].double_value = m_indicatorTuner.adWES.volHigh; // InpVolHigh
|
|
params[5].type = TYPE_DOUBLE;
|
|
params[5].double_value = m_indicatorTuner.adWES.rangeClimax; // InpRangeClimax
|
|
params[6].type = TYPE_DOUBLE;
|
|
params[6].double_value = m_indicatorTuner.adWES.rangeSignificant; // InpRangeSignificant
|
|
params[7].type = TYPE_DOUBLE;
|
|
params[7].double_value = m_indicatorTuner.adWES.stVolRatio; // InpSTVolRatio
|
|
params[8].type = TYPE_DOUBLE;
|
|
params[8].double_value = m_indicatorTuner.adWES.atr; // InpATR
|
|
params[9].type = TYPE_INT;
|
|
params[9].integer_value = 0; // InpContextMode - DO NOT tune
|
|
params[10].type = TYPE_INT;
|
|
params[10].integer_value = 5; // InpSessionType - DO NOT tune
|
|
params[11].type = TYPE_INT;
|
|
params[11].integer_value = 1; // InpSessionCount - DO NOT tune
|
|
params[12].type = TYPE_DOUBLE;
|
|
params[12].double_value = m_indicatorTuner.adWES.touchATR; // InpTouchATR
|
|
params[13].type = TYPE_DOUBLE;
|
|
params[13].double_value = m_indicatorTuner.adWES.arMinATR; // InpARMinATR
|
|
params[14].type = TYPE_INT;
|
|
params[14].integer_value = m_indicatorTuner.adWES.maxRangeBars; // InpMaxRangeBars
|
|
//--- InpShowLabels/InpShowZones - forced OFF, and deliberately NOT tunable. This handle exists purely
|
|
//--- to read buffers as network features; it is never the user's chart indicator. Left at their own
|
|
//--- `true` defaults the indicator would litter the traded chart with AWY_-prefixed labels and range
|
|
//--- rectangles that the EA does not own and its OnDeinit chart sweep does not know to remove.
|
|
params[15].type = TYPE_BOOL;
|
|
params[15].integer_value = 0; // InpShowLabels
|
|
params[16].type = TYPE_BOOL;
|
|
params[16].integer_value = 0; // InpShowZones
|
|
if(!m_ADWyckoffEventStream.Create(m_symbol.Name(), m_period, IND_CUSTOM, 17, params))
|
|
{
|
|
printf(__FUNCTION__ + ": error initializing object");
|
|
return (false);
|
|
}
|
|
m_ADWyckoffEventStream.NumBuffers(14);
|
|
//--- ok
|
|
return (true);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Initialize AD Wyckoff Failed Structure (CustomIndicators\ADWyckoffFailedStructure.mq5) |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::InitADWyckoffFailedStructure(CIndicators * indicators, bool addToCollection)
|
|
{
|
|
//--- check pointer
|
|
if(indicators == NULL)
|
|
return (false);
|
|
//--- add object to collection
|
|
if(addToCollection && !indicators.Add(GetPointer(m_ADWyckoffFailedStructure)))
|
|
{
|
|
printf(__FUNCTION__ + ": error adding object");
|
|
return (false);
|
|
}
|
|
//--- initialize object; params[1..] mirror ADWyckoffFailedStructure.mq5's own input order exactly
|
|
MqlParam params[12];
|
|
params[0].type = TYPE_STRING;
|
|
params[0].string_value = WARRIOR_CI("ADWyckoffFailedStructure");
|
|
params[1].type = TYPE_INT;
|
|
params[1].integer_value = m_indicatorTuner.adWFS.lookback; // InpLookbackPeriod
|
|
params[2].type = TYPE_INT;
|
|
params[2].integer_value = m_indicatorTuner.adWFS.zigzagStrength; // InpZigZagStrength
|
|
params[3].type = TYPE_DOUBLE;
|
|
params[3].double_value = m_indicatorTuner.adWFS.volClimax; // InpVolumeClimaxMultiplier
|
|
params[4].type = TYPE_DOUBLE;
|
|
params[4].double_value = m_indicatorTuner.adWFS.volHigh; // InpVolumeHighMultiplier
|
|
params[5].type = TYPE_DOUBLE;
|
|
params[5].double_value = m_indicatorTuner.adWFS.rangeClimax; // InpRangeClimaxMultiplier
|
|
params[6].type = TYPE_DOUBLE;
|
|
params[6].double_value = m_indicatorTuner.adWFS.rangeSignificant; // InpRangeSignificantMult
|
|
params[7].type = TYPE_DOUBLE;
|
|
params[7].double_value = m_indicatorTuner.adWFS.stVolRatio; // InpSTVolumeRatio
|
|
params[8].type = TYPE_DOUBLE;
|
|
params[8].double_value = m_indicatorTuner.adWFS.atrMult; // InpATRMultiplier
|
|
params[9].type = TYPE_INT;
|
|
params[9].integer_value = 0; // InpContextMode - DO NOT tune
|
|
params[10].type = TYPE_INT;
|
|
params[10].integer_value = 5; // InpSessionType - DO NOT tune
|
|
params[11].type = TYPE_INT;
|
|
params[11].integer_value = 1; // InpSessionCount - DO NOT tune
|
|
if(!m_ADWyckoffFailedStructure.Create(m_symbol.Name(), m_period, IND_CUSTOM, 12, params))
|
|
{
|
|
printf(__FUNCTION__ + ": error initializing object");
|
|
return (false);
|
|
}
|
|
m_ADWyckoffFailedStructure.NumBuffers(5);
|
|
//--- ok
|
|
return (true);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Initialize AD Wyckoff Significant Bar Inversion (CustomIndicators\ADWyckoffSignificantBarInversion.mq5) |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::InitADWyckoffSignificantBarInversion(CIndicators * indicators, bool addToCollection)
|
|
{
|
|
//--- check pointer
|
|
if(indicators == NULL)
|
|
return (false);
|
|
//--- add object to collection
|
|
if(addToCollection && !indicators.Add(GetPointer(m_ADWyckoffSignificantBarInversion)))
|
|
{
|
|
printf(__FUNCTION__ + ": error adding object");
|
|
return (false);
|
|
}
|
|
//--- initialize object; params[1..] mirror ADWyckoffSignificantBarInversion.mq5's own input order exactly
|
|
MqlParam params[8];
|
|
params[0].type = TYPE_STRING;
|
|
params[0].string_value = WARRIOR_CI("ADWyckoffSignificantBarInversion");
|
|
params[1].type = TYPE_INT;
|
|
params[1].integer_value = m_indicatorTuner.adWSBI.lookback; // InpLookback
|
|
params[2].type = TYPE_DOUBLE;
|
|
params[2].double_value = m_indicatorTuner.adWSBI.rangeSignificant; // InpRangeSignificant
|
|
params[3].type = TYPE_DOUBLE;
|
|
params[3].double_value = m_indicatorTuner.adWSBI.volumeHigh; // InpVolumeHigh
|
|
params[4].type = TYPE_DOUBLE;
|
|
params[4].double_value = m_indicatorTuner.adWSBI.atr; // InpATR
|
|
params[5].type = TYPE_INT;
|
|
params[5].integer_value = 0; // InpContextMode - DO NOT tune
|
|
params[6].type = TYPE_INT;
|
|
params[6].integer_value = 5; // InpSessionType - DO NOT tune
|
|
params[7].type = TYPE_INT;
|
|
params[7].integer_value = 1; // InpSessionCount - DO NOT tune
|
|
if(!m_ADWyckoffSignificantBarInversion.Create(m_symbol.Name(), m_period, IND_CUSTOM, 8, params))
|
|
{
|
|
printf(__FUNCTION__ + ": error initializing object");
|
|
return (false);
|
|
}
|
|
m_ADWyckoffSignificantBarInversion.NumBuffers(5);
|
|
//--- ok
|
|
return (true);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Initialize AD ZigZag (CustomIndicators\ADZigZag.mq5) - the |
|
|
//| training-label source (see m_ADZigZag's declaration comment). |
|
|
//| Always run at its own stock defaults (Depth=12, Deviation=5, |
|
|
//| Backstep=3) - unlike the AD* feature indicators above, this has |
|
|
//| no tunable-param struct and is never touched by AutoTuneIndicators.|
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalAIBase::InitADZigZag(CIndicators * indicators, bool addToCollection)
|
|
{
|
|
//--- check pointer
|
|
if(indicators == NULL)
|
|
return (false);
|
|
//--- add object to collection
|
|
if(addToCollection && !indicators.Add(GetPointer(m_ADZigZag)))
|
|
{
|
|
printf(__FUNCTION__ + ": error adding object");
|
|
return (false);
|
|
}
|
|
//--- initialize object; params[1..] mirror ADZigZag.mq5's own input order exactly - stock defaults,
|
|
//--- intentionally not sourced from a tunable params struct (see this function's declaration comment)
|
|
MqlParam params[4];
|
|
params[0].type = TYPE_STRING;
|
|
params[0].string_value = WARRIOR_CI("ADZigZag");
|
|
params[1].type = TYPE_INT;
|
|
params[1].integer_value = 12; // InpDepth
|
|
params[2].type = TYPE_INT;
|
|
params[2].integer_value = 5; // InpDeviation
|
|
params[3].type = TYPE_INT;
|
|
params[3].integer_value = 3; // InpBackstep
|
|
if(!m_ADZigZag.Create(m_symbol.Name(), m_period, IND_CUSTOM, 4, params))
|
|
{
|
|
printf(__FUNCTION__ + ": error initializing object");
|
|
return (false);
|
|
}
|
|
// Must match ADZigZag.mq5's #property indicator_buffers exactly (3: main ZigZag buffer + 2
|
|
// internal INDICATOR_CALCULATIONS buffers), even though only buffer 0 is ever read via
|
|
// GetData() - see the working AD Wyckoff indicators' InitAD*() for the same pattern.
|
|
m_ADZigZag.NumBuffers(3);
|
|
//--- ok
|
|
return (true);
|
|
}
|
|
#endif // WARRIOR_AIBASE_FEATURES_MQH
|