forked from animatedread/Warrior_EA
A resumed META model hot-looped pass 1 (0->100% scan oscillation, silent for
3 minutes until the stall reporter fired) because EVERY window failed at the
first AD/Wyckoff feature: the init-time param adoption called
ReInitADIndicators unconditionally, destroying five freshly-calculating
indicator instances to recreate them with BYTE-IDENTICAL params (verified by
parsing the .nnw header - the MI tuner had kept the configured settings), at
process start, on a box with 1 GB free of 31. The replacements sat cold for
6+ minutes while full-history resweeps starved the indicator threads harder.
- AdoptIndicatorParams: installs a loaded param set into the tuner and
rebuilds handles ONLY when the set actually differs from what the live
indicators run. Both call sites (resume init + panel reload) use it.
- Resumed models get the same 3 warm-up passes as fresh ones. The skip was
the shared root cause of the cold-ATR (ba13eef), cold-AD (2026-08-11) and
this incident - custom indicators recompute from scratch every process
start regardless of what the .nnw proves.
- Cold-sweep backoff: a pass-1 sweep in which every window failed on a
TRANSIENT cause arms a 5s era-start pause instead of an immediate
full-history resweep, so the retry loop stops consuming the CPU/memory the
warming indicators need. The stall reporter names the backoff branch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1690 lines
92 KiB
MQL5
1690 lines
92 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(void)
|
|
{
|
|
int worst = INT_MAX;
|
|
if(m_useMA)
|
|
worst = (int)MathMin(worst, m_MA.BarsCalculated());
|
|
if(m_useRSI)
|
|
worst = (int)MathMin(worst, m_RSI.BarsCalculated());
|
|
if(m_useMACD)
|
|
worst = (int)MathMin(worst, m_MACDFeature.BarsCalculated());
|
|
if(m_useIchimoku)
|
|
worst = (int)MathMin(worst, m_Ichimoku.BarsCalculated());
|
|
if(m_useADCumulativeDelta)
|
|
worst = (int)MathMin(worst, m_ADCumulativeDelta.BarsCalculated());
|
|
if(m_useADShorteningOfThrust)
|
|
worst = (int)MathMin(worst, m_ADShorteningOfThrust.BarsCalculated());
|
|
if(m_useADWyckoffEventStream)
|
|
worst = (int)MathMin(worst, m_ADWyckoffEventStream.BarsCalculated());
|
|
if(m_useADWyckoffFailedStructure)
|
|
worst = (int)MathMin(worst, m_ADWyckoffFailedStructure.BarsCalculated());
|
|
if(m_useADWyckoffSignificantBarInversion)
|
|
worst = (int)MathMin(worst, m_ADWyckoffSignificantBarInversion.BarsCalculated());
|
|
return (worst == INT_MAX) ? -1 : worst;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//+------------------------------------------------------------------+
|
|
//| 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.
|
|
int closeBars = m_useIchimoku ? barIndex + m_indicatorTuner.ichiKijun : 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)
|
|
{
|
|
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(barIndex + m_indicatorTuner.ichiKijun))
|
|
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). |
|
|
//+------------------------------------------------------------------+
|
|
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;
|
|
//--- 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;
|
|
}
|
|
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;
|
|
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;
|
|
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;
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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))
|
|
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)) // 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)) // 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)) // 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)) // 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;
|
|
}
|
|
//--- 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)
|
|
{
|
|
if(ind.GetData(0, 0) != EMPTY_VALUE)
|
|
return false;
|
|
m_featureFailTransient = true;
|
|
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
|