2026-08-16 13:39:00 -04:00
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| AltData.mqh |
|
|
|
|
|
//| AnimateDread |
|
2026-08-22 00:30:14 -04:00
|
|
|
//| research/altdata (Python) collects sources that are NOT |
|
|
|
|
|
//| derivable from any chart series - CFTC COT positioning, the VIX |
|
|
|
|
|
//| complex, macro series - stamps every row with the date it became |
|
|
|
|
|
//| PUBLICLY KNOWABLE, as-of joins them onto calendar days, and |
|
|
|
|
|
//| exports Common\Files\Warrior_EA\AltData\{SYMBOL}_{TF}.csv: |
|
2026-08-16 13:39:00 -04:00
|
|
|
//+------------------------------------------------------------------+
|
2026-08-24 01:31:14 -04:00
|
|
|
#ifndef WARRIOR_ALTDATA_MQH
|
|
|
|
|
#define WARRIOR_ALTDATA_MQH
|
refactor(stdlib): one quantile definition, from Math\Stat
The codebase had THREE conventions for the same statistic. AltData took a
true median; the barrier horizon and the derived input window took the
upper of the two middle values; the MI terciles and the barrier stop
ladder used nearest-rank indexing. All four now go through MathMedian /
MathQuantile, which is R's type 7 and the library's one answer.
System\AltData.mqh column median -> MathMedian (exact, no change)
AIBase\Labels.mqh swing median -> MathMedian
leg-range med -> MathMedian
stop ladder -> MathQuantile, read in one call
AIBase\Topology.mqh window median -> MathMedian
AIBase\AutoTune.mqh MI terciles -> MathQuantile + MathMin/MathMax
Signals\SignalSessionFilter DST last Sunday-> CDateTime::DaysInMonth()
gaps[]/legs[] change from int to double so MathMedian can read them; the
values are bar counts either way.
VALUES MOVE. Even-sample medians shift by half a bin and the quantile
reads interpolate, so the barrier geometry and the derived input window
can land on different rungs - re-keying fingerprints and forcing a
retrain. Accepted deliberately: stdlib consistency was the ask, and three
private conventions for one statistic is what it buys out.
Two YAGNI finds fell out of the ladder rewrite. MathQuantile sorts its own
copy, so DeriveBarrierGeometry no longer sorts up[]/dn[] in place - which
means upUnsorted[], a full array copy kept only to undo that sort, is
gone. ArraySort(up) had no consumer needing order at all; it was pure
work. The library call also gets a failure guard the hand-rolled indexing
never needed but the ladder read does.
Verified while here: Math\Stat\Math.mqh's MathAbs/MathMax/MathSqrt/MathPow
and friends are ARRAY overloads, not scalar redefinitions, so pulling it
into the translation unit shadows no builtin.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 20:16:03 -04:00
|
|
|
#include <Math\Stat\Math.mqh>
|
fix(altdata): robustness pass for arbitrary symbol/timeframe - validation + error handling
Systematic audit of the alt-data stack against "any symbol, any timeframe",
prompted by the H4 surprise. Findings, each fixed:
CAPACITY: ALTDATA_MAX_FEATURES was 16 with FX symbols already at 15 - the next
added column would have been silently truncated by a MathMin. Raised to 32,
pin-chars 512 -> 1024.
SYMBOL NAMES: the panel reload path re-derived symbol/timeframe by splitting
the file path on its FIRST underscore - mis-parsing every symbol containing
one (OANDA-style EUR_USD and US_500 are in our own alias lists) and knowing
only three timeframes. It now stores the (symbol, period) Load() was called
with and reuses them verbatim. Path-hostile characters in broker symbols
("EUR/USD") are sanitized by a shared AltDataFileSymbol() used by the panel,
the fetcher and TunedPeriods, so a slash cannot route a write into an
unintended subfolder.
DOWNLOAD VALIDATION: every FRED-family fetch now enforces a per-series
plausibility band (VIX 1-200, yields -5..30, CPI index 20-1000, ...) -
StringToDouble on transport garbage returns 0.0, and one absurd value poisons
every change/percentile feature computed across it (the BatchNorm NaN-latch
incident came from exactly one huge-but-finite input). Rejected rows are
counted and reported, never dropped silently.
LOUD EMPTINESS: a successful response with zero observations on an empty
cache now says so - naming the series (wrong id / format drift) or the COT
predicate (the unverified like-clauses) instead of leaving 0-filled features
unexplained. GEX gains a truncation guard: a day-over-day contract-count
collapse >50% is the fingerprint of a partial 13 MB download, not of markets,
and is skipped rather than recorded as a plausible-but-wrong number.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 17:59:10 -04:00
|
|
|
//--- 2026-08-16: raised 16 -> 32. The wire-everything policy put FX symbols at 15
|
|
|
|
|
//--- features; at 16 the NEXT added column would have been silently truncated by the
|
|
|
|
|
//--- MathMin below - exactly the class of quiet degradation this module must never do.
|
|
|
|
|
#define ALTDATA_MAX_FEATURES 32
|
|
|
|
|
#define ALTDATA_MAX_PIN_CHARS 1024
|
|
|
|
|
|
feat(pool,mi): one feature layout fleet-wide, and the keep-screen stops self-disabling on a cold start
TWO CHANGES, BOTH RETRAIN-FORCING BY INTENT.
1. SP500 was training alone, and one alt-data column was the reason.
The alt block's width joins the model fingerprint, and the pool reader only
adopts peer rows whose fingerprint and width match. The exporter gives each
instrument the series that apply to it - FX 15 columns, metals/oil 14, SP500
13 - so the fleet ran as three incompatible pools:
EURUSD/USDJPY/USDCAD adopt ~57-60k peer rows each
XAUUSD/XTIUSD adopt 6.4k / 20.3k
SP500 "EVERY peer file was REJECTED, so this chart is
training alone" - 0 rows
SP500 therefore trained on 2279 independent observations against a 600-wide
input with its first layer floored at 16, printing its own "expect
overfitting" warning. It is the one chart with no pool and the worst
capacity ratio in the fleet by a factor of three.
Fresh models now pin ALTDATA_FLEET_COLUMNS - the 12-column intersection -
instead of their own file header. An existing model still adopts its .cfg
pin, so this re-keys nothing that is already trained.
Intersection rather than union: filling an absent series with its median
makes that column constant per instrument, which lets a pooled model
identify the source instrument and stop learning the shared mechanism. It
is also 6 columns narrower. Cost is six columns whose retained information
is UNMEASURED - the keep-screen reports a bitmask nothing has mapped back
to names.
2. The MI keep-screen disabled itself for the whole run on any cold start.
ReportFeatureLabelInformation set m_miReportDone on ENTRY. On a cold start
the label cache is allocated before it is filled, so BuildMiSample finds no
row carrying a resolved label and returns 0 - a sixth exit, and the only
one the 8c1266d instrumentation did not cover, which is why it printed
nothing. observed then stayed -1, the permutation loop never iterated, and
the report emitted "-1.00000 nats over 0 permutations" beside a plausible
"strongest single feature 0.05979" that was a STALE m_miBestColumn from an
earlier scoring call. The first ensemble member propagated the latch to
g_ensembleChartMiReportDone and silenced every member on the chart.
The flag now latches only once a measurement exists. A short sample is
reported as a deferral naming the two numbers that identify it (cached bars
vs bars carrying a resolved label) and retried, up to
MI_REPORT_MAX_ATTEMPTS.
Build tag -> fleet-pool-v1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 17:41:14 -04:00
|
|
|
//--- THE FLEET COLUMN SET, AND WHY IT IS AN INTERSECTION (2026-08-26).
|
|
|
|
|
//---
|
|
|
|
|
//--- Each exported {SYMBOL}_D1.csv carries whatever series apply to that instrument, so the fleet ran
|
|
|
|
|
//--- three different alt-data widths: FX 15 (COT triple), metals/oil 14 (implied-vol pair), SP500 13
|
|
|
|
|
//--- (cot_spec_net). The alt block's width joins the model fingerprint, and CTrainPoolReader only
|
|
|
|
|
//--- adopts peer rows whose fingerprint AND width match - so those three sets were three separate
|
|
|
|
|
//--- training pools. SP500 was alone in its own: "EVERY peer file was REJECTED, so this chart is
|
|
|
|
|
//--- training alone", 0 pooled rows, 2279 independent observations against a 600-wide input, the
|
|
|
|
|
//--- first layer floored at 16, and the EA's own capacity warning reading "expect overfitting". It
|
|
|
|
|
//--- was the one chart with no pool and the one chart being asked to be more precise.
|
|
|
|
|
//---
|
|
|
|
|
//--- INTERSECTION, NOT UNION, and the reason is not economy. A union would fill a series an
|
|
|
|
|
//--- instrument does not have with that column's median, making it CONSTANT for that instrument -
|
|
|
|
|
//--- so a pooled model could identify the source instrument from its alt block and would stop
|
|
|
|
|
//--- learning the shared mechanism, which is the entire point of pooling. The union is also 6
|
|
|
|
|
//--- columns wider, moving the capacity ratio the wrong way. The intersection wins on both axes.
|
|
|
|
|
//---
|
|
|
|
|
//--- COST, STATED PLAINLY: FX gives up cot_idx_1y/cot_idx_3y/cot_chg_4w, metals and oil give up
|
|
|
|
|
//--- ivol_chg5/ivol, SP500 gives up cot_spec_net. Whether any of those six carried retained
|
|
|
|
|
//--- information is NOT known - the keep-screen reports a column BITMASK, and no run has yet mapped
|
|
|
|
|
//--- its bits back to names. Do not read this constant as evidence they were worthless; it trades
|
|
|
|
|
//--- six unproven columns for a pool the starved chart has never had.
|
|
|
|
|
#define ALTDATA_FLEET_COLUMNS "vix_chg5,vix,usd_chg5,eia_stk_idx1y,eia_stk_chg4,eia_util,mac_y10,mac_curve,mac_bei,mac_gap,mac_cpi,mac_unemp"
|
|
|
|
|
|
2026-08-22 00:25:52 -04:00
|
|
|
//--- Symbol -> safe file-name token, shared by every consumer that builds a path from a broker
|
|
|
|
|
//--- symbol (feature files, GEX recorder, TunedPeriods).
|
fix(altdata): robustness pass for arbitrary symbol/timeframe - validation + error handling
Systematic audit of the alt-data stack against "any symbol, any timeframe",
prompted by the H4 surprise. Findings, each fixed:
CAPACITY: ALTDATA_MAX_FEATURES was 16 with FX symbols already at 15 - the next
added column would have been silently truncated by a MathMin. Raised to 32,
pin-chars 512 -> 1024.
SYMBOL NAMES: the panel reload path re-derived symbol/timeframe by splitting
the file path on its FIRST underscore - mis-parsing every symbol containing
one (OANDA-style EUR_USD and US_500 are in our own alias lists) and knowing
only three timeframes. It now stores the (symbol, period) Load() was called
with and reuses them verbatim. Path-hostile characters in broker symbols
("EUR/USD") are sanitized by a shared AltDataFileSymbol() used by the panel,
the fetcher and TunedPeriods, so a slash cannot route a write into an
unintended subfolder.
DOWNLOAD VALIDATION: every FRED-family fetch now enforces a per-series
plausibility band (VIX 1-200, yields -5..30, CPI index 20-1000, ...) -
StringToDouble on transport garbage returns 0.0, and one absurd value poisons
every change/percentile feature computed across it (the BatchNorm NaN-latch
incident came from exactly one huge-but-finite input). Rejected rows are
counted and reported, never dropped silently.
LOUD EMPTINESS: a successful response with zero observations on an empty
cache now says so - naming the series (wrong id / format drift) or the COT
predicate (the unverified like-clauses) instead of leaving 0-filled features
unexplained. GEX gains a truncation guard: a day-over-day contract-count
collapse >50% is the fingerprint of a partial 13 MB download, not of markets,
and is skipped rather than recorded as a plausible-but-wrong number.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 17:59:10 -04:00
|
|
|
string AltDataFileSymbol(string symbol)
|
|
|
|
|
{
|
|
|
|
|
string out = symbol;
|
|
|
|
|
StringReplace(out, "/", "-");
|
|
|
|
|
StringReplace(out, "\\", "-");
|
|
|
|
|
StringReplace(out, ":", "-");
|
|
|
|
|
StringReplace(out, "*", "-");
|
|
|
|
|
StringReplace(out, "?", "-");
|
|
|
|
|
StringReplace(out, "\"", "-");
|
|
|
|
|
StringReplace(out, "<", "-");
|
|
|
|
|
StringReplace(out, ">", "-");
|
|
|
|
|
StringReplace(out, "|", "-");
|
|
|
|
|
return out;
|
|
|
|
|
}
|
2026-08-16 13:39:00 -04:00
|
|
|
|
|
|
|
|
class CAltDataPanel
|
|
|
|
|
{
|
|
|
|
|
protected:
|
|
|
|
|
datetime m_rowTime[]; // row date (server-naive 00:00), ascending
|
|
|
|
|
double m_vals[]; // rows x m_fileCount, EMPTY_VALUE = blank cell
|
|
|
|
|
int m_rows;
|
|
|
|
|
int m_fileCount; // columns in the loaded file
|
2026-08-22 00:25:52 -04:00
|
|
|
//--- Per-file-column MEDIAN over the covered range, used to fill bars the file cannot answer
|
|
|
|
|
//--- for. WHY NOT ZERO, which is what this did until 2026-08-17.
|
fix(altdata): median-fill instead of zero-fill, and a one-shot feature-vector autopsy
ALT-DATA AUDIT. The files themselves are healthy - all six symbols, 6,073 daily
rows, 2010-01-01 to 2026-08-17, no constant or degenerate columns, sane tails
(mac_cpi/mac_unemp flat ~47d is monthly data behaving correctly). The problem is
not the data, it is what happens where the data ISN'T.
CAltDataPanel::Features() returned an all-ZERO vector for any bar older than the
file's first row, and left blank cells at 0 too. Both were deliberate ('the block
is additive context and must degrade, never reject the bar') and that reasoning
holds for the CHANGE columns - but half these features are LEVELS: vix, ivol,
mac_y10, mac_cpi, mac_unemp, eia_util. For a level, 0 is not a missing reading,
it is an impossible one far outside the series' range. VIX does not visit zero.
And the spike lands in exactly the wrong place. Every alt file starts 2010-01-01
while the charts run far deeper - USDJPY H4 reaches ~1994, roughly HALF its
history - so 'alt block is all zeros' is precisely the predicate 'this bar is
older than 2010'. The IS/OOS split is chronological, so that predicate covers
~half of IS and none of OOS: an in-sample feature guaranteed to be useless
out-of-sample, and a bimodal input for the first BatchNorm to normalise. Not a
lookahead leak - a distribution corruption, which is quieter and was never
reported anywhere.
Now filled with the column MEDIAN over the covered range. A constant cannot leak
whatever its source - it takes the same value on every pre-coverage bar, so it
carries no information about which of those bars won - which is what makes a
median computed over later data legitimate here. Median not mean because the
series are skewed. Blank cells get the same treatment (eia_stk_idx1y alone has
181 blanks in 6,073 rows) and the count is now logged at load.
THE BACKOFF WAS ALREADY THERE AND WAS DEAD. Training.mqh arms m_coldSweepTick on
m_featureFailTransient, but only the open/ATR guards ever set that flag, so
f0cf659's cold ADMovingAverage looked PERMANENT and the sweep re-ran at full
speed forever. Setting the flag in the indicator guards revives the mechanism
that was already designed for this; no second backoff was needed and the one I
first wrote has been removed in favour of it.
SELF-HEALING, as asked. ReportFeatureHealth() runs once, the first time pass 1
produces usable windows, samples ~400 bars spread across the whole training range
and names every feature slot that is CONSTANT or mostly-zero, tagging alt-block
slots as alt[i]. Both of today's failures were the same shape - a block silently
produces nothing while every downstream number stays plausible - and neither an
accuracy figure nor a model can tell 'this feature is always 0' from 'this
feature is genuinely 0 here'. Evenly spaced sampling so a block that dies only in
deep history is caught as surely as one dead everywhere. A report, not a gate:
a rare-flag feature can be legitimately constant, and refusing to train would
turn a diagnostic into an outage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 12:53:51 -04:00
|
|
|
double m_median[];
|
2026-08-16 13:39:00 -04:00
|
|
|
string m_fileNames[]; // file column names, file order
|
|
|
|
|
string m_pinnedNames[]; // pinned name list ("" = none): the OUTPUT contract
|
|
|
|
|
int m_pinnedCount;
|
|
|
|
|
int m_map[]; // output slot -> file column (-1 = 0-fill)
|
|
|
|
|
string m_loadedFile;
|
fix(altdata): robustness pass for arbitrary symbol/timeframe - validation + error handling
Systematic audit of the alt-data stack against "any symbol, any timeframe",
prompted by the H4 surprise. Findings, each fixed:
CAPACITY: ALTDATA_MAX_FEATURES was 16 with FX symbols already at 15 - the next
added column would have been silently truncated by a MathMin. Raised to 32,
pin-chars 512 -> 1024.
SYMBOL NAMES: the panel reload path re-derived symbol/timeframe by splitting
the file path on its FIRST underscore - mis-parsing every symbol containing
one (OANDA-style EUR_USD and US_500 are in our own alias lists) and knowing
only three timeframes. It now stores the (symbol, period) Load() was called
with and reuses them verbatim. Path-hostile characters in broker symbols
("EUR/USD") are sanitized by a shared AltDataFileSymbol() used by the panel,
the fetcher and TunedPeriods, so a slash cannot route a write into an
unintended subfolder.
DOWNLOAD VALIDATION: every FRED-family fetch now enforces a per-series
plausibility band (VIX 1-200, yields -5..30, CPI index 20-1000, ...) -
StringToDouble on transport garbage returns 0.0, and one absurd value poisons
every change/percentile feature computed across it (the BatchNorm NaN-latch
incident came from exactly one huge-but-finite input). Rejected rows are
counted and reported, never dropped silently.
LOUD EMPTINESS: a successful response with zero observations on an empty
cache now says so - naming the series (wrong id / format drift) or the COT
predicate (the unverified like-clauses) instead of leaving 0-filled features
unexplained. GEX gains a truncation guard: a day-over-day contract-count
collapse >50% is the fingerprint of a partial 13 MB download, not of markets,
and is skipped rather than recorded as a plausible-but-wrong number.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 17:59:10 -04:00
|
|
|
string m_loadedSymbol; // what Load() was CALLED with - reload uses these
|
|
|
|
|
ENUM_TIMEFRAMES m_loadedPeriod; // verbatim, never re-derived from the file path
|
2026-08-16 13:39:00 -04:00
|
|
|
datetime m_lastLoadTry;
|
|
|
|
|
bool m_loggedMissing;
|
|
|
|
|
|
|
|
|
|
void BuildMap(void)
|
|
|
|
|
{
|
|
|
|
|
int outCount = FeatureCount();
|
|
|
|
|
ArrayResize(m_map, outCount);
|
|
|
|
|
for(int j = 0; j < outCount; j++)
|
|
|
|
|
{
|
|
|
|
|
string want = (m_pinnedCount > 0) ? m_pinnedNames[j] : m_fileNames[j];
|
|
|
|
|
m_map[j] = -1;
|
|
|
|
|
for(int c = 0; c < m_fileCount; c++)
|
|
|
|
|
if(m_fileNames[c] == want)
|
|
|
|
|
{
|
|
|
|
|
m_map[j] = c;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if(m_map[j] < 0 && m_pinnedCount > 0)
|
|
|
|
|
PrintFormat("AltData: pinned feature '%s' not present in %s - that input 0-fills "
|
|
|
|
|
"until the export carries it again.", want, m_loadedFile);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
CAltDataPanel(void) : m_rows(0), m_fileCount(0), m_pinnedCount(0),
|
fix(altdata): robustness pass for arbitrary symbol/timeframe - validation + error handling
Systematic audit of the alt-data stack against "any symbol, any timeframe",
prompted by the H4 surprise. Findings, each fixed:
CAPACITY: ALTDATA_MAX_FEATURES was 16 with FX symbols already at 15 - the next
added column would have been silently truncated by a MathMin. Raised to 32,
pin-chars 512 -> 1024.
SYMBOL NAMES: the panel reload path re-derived symbol/timeframe by splitting
the file path on its FIRST underscore - mis-parsing every symbol containing
one (OANDA-style EUR_USD and US_500 are in our own alias lists) and knowing
only three timeframes. It now stores the (symbol, period) Load() was called
with and reuses them verbatim. Path-hostile characters in broker symbols
("EUR/USD") are sanitized by a shared AltDataFileSymbol() used by the panel,
the fetcher and TunedPeriods, so a slash cannot route a write into an
unintended subfolder.
DOWNLOAD VALIDATION: every FRED-family fetch now enforces a per-series
plausibility band (VIX 1-200, yields -5..30, CPI index 20-1000, ...) -
StringToDouble on transport garbage returns 0.0, and one absurd value poisons
every change/percentile feature computed across it (the BatchNorm NaN-latch
incident came from exactly one huge-but-finite input). Rejected rows are
counted and reported, never dropped silently.
LOUD EMPTINESS: a successful response with zero observations on an empty
cache now says so - naming the series (wrong id / format drift) or the COT
predicate (the unverified like-clauses) instead of leaving 0-filled features
unexplained. GEX gains a truncation guard: a day-over-day contract-count
collapse >50% is the fingerprint of a partial 13 MB download, not of markets,
and is skipped rather than recorded as a plausible-but-wrong number.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 17:59:10 -04:00
|
|
|
m_loadedFile(""), m_loadedSymbol(""), m_loadedPeriod(PERIOD_CURRENT),
|
|
|
|
|
m_lastLoadTry(0), m_loggedMissing(false) {}
|
2026-08-16 13:39:00 -04:00
|
|
|
|
|
|
|
|
datetime LastDate(void) const { return m_rows > 0 ? m_rowTime[m_rows - 1] : 0; }
|
|
|
|
|
//--- The width contract: pinned list is the authority once set; otherwise the file decides.
|
|
|
|
|
int FeatureCount(void) const { return m_pinnedCount > 0 ? m_pinnedCount : m_fileCount; }
|
|
|
|
|
|
|
|
|
|
string NamesCsv(void) const
|
|
|
|
|
{
|
|
|
|
|
string csv = "";
|
|
|
|
|
int n = FeatureCount();
|
|
|
|
|
for(int j = 0; j < n; j++)
|
|
|
|
|
csv += (j > 0 ? "," : "") + (m_pinnedCount > 0 ? m_pinnedNames[j] : m_fileNames[j]);
|
|
|
|
|
return csv;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SetPinnedNames(string csv)
|
|
|
|
|
{
|
|
|
|
|
m_pinnedCount = (csv == "") ? 0 : StringSplit(csv, ',', m_pinnedNames);
|
|
|
|
|
if(m_pinnedCount > ALTDATA_MAX_FEATURES)
|
|
|
|
|
{
|
|
|
|
|
PrintFormat("AltData: pinned list of %d exceeds the %d-feature cap - truncating. "
|
|
|
|
|
"This should never happen; check the .cfg.", m_pinnedCount, ALTDATA_MAX_FEATURES);
|
|
|
|
|
m_pinnedCount = ALTDATA_MAX_FEATURES;
|
|
|
|
|
}
|
|
|
|
|
if(m_fileCount > 0)
|
|
|
|
|
BuildMap();
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-16 17:53:41 -04:00
|
|
|
//--- Parse Common\Files\Warrior_EA\AltData\{SYMBOL}_{TF}.csv, FALLING BACK to the
|
2026-08-22 00:25:52 -04:00
|
|
|
//--- {SYMBOL}_D1.csv the fetcher maintains.
|
2026-08-16 13:39:00 -04:00
|
|
|
bool Load(string symbol, ENUM_TIMEFRAMES period)
|
|
|
|
|
{
|
|
|
|
|
m_lastLoadTry = TimeCurrent();
|
|
|
|
|
string tf = StringSubstr(EnumToString(period), 7); // PERIOD_D1 -> D1
|
fix(altdata): robustness pass for arbitrary symbol/timeframe - validation + error handling
Systematic audit of the alt-data stack against "any symbol, any timeframe",
prompted by the H4 surprise. Findings, each fixed:
CAPACITY: ALTDATA_MAX_FEATURES was 16 with FX symbols already at 15 - the next
added column would have been silently truncated by a MathMin. Raised to 32,
pin-chars 512 -> 1024.
SYMBOL NAMES: the panel reload path re-derived symbol/timeframe by splitting
the file path on its FIRST underscore - mis-parsing every symbol containing
one (OANDA-style EUR_USD and US_500 are in our own alias lists) and knowing
only three timeframes. It now stores the (symbol, period) Load() was called
with and reuses them verbatim. Path-hostile characters in broker symbols
("EUR/USD") are sanitized by a shared AltDataFileSymbol() used by the panel,
the fetcher and TunedPeriods, so a slash cannot route a write into an
unintended subfolder.
DOWNLOAD VALIDATION: every FRED-family fetch now enforces a per-series
plausibility band (VIX 1-200, yields -5..30, CPI index 20-1000, ...) -
StringToDouble on transport garbage returns 0.0, and one absurd value poisons
every change/percentile feature computed across it (the BatchNorm NaN-latch
incident came from exactly one huge-but-finite input). Rejected rows are
counted and reported, never dropped silently.
LOUD EMPTINESS: a successful response with zero observations on an empty
cache now says so - naming the series (wrong id / format drift) or the COT
predicate (the unverified like-clauses) instead of leaving 0-filled features
unexplained. GEX gains a truncation guard: a day-over-day contract-count
collapse >50% is the fingerprint of a partial 13 MB download, not of markets,
and is skipped rather than recorded as a plausible-but-wrong number.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 17:59:10 -04:00
|
|
|
string fileSym = AltDataFileSymbol(symbol);
|
|
|
|
|
string name = "Warrior_EA\\AltData\\" + fileSym + "_" + tf + ".csv";
|
2026-08-16 17:53:41 -04:00
|
|
|
if(!FileIsExist(name, FILE_COMMON) && tf != "D1")
|
|
|
|
|
{
|
fix(altdata): robustness pass for arbitrary symbol/timeframe - validation + error handling
Systematic audit of the alt-data stack against "any symbol, any timeframe",
prompted by the H4 surprise. Findings, each fixed:
CAPACITY: ALTDATA_MAX_FEATURES was 16 with FX symbols already at 15 - the next
added column would have been silently truncated by a MathMin. Raised to 32,
pin-chars 512 -> 1024.
SYMBOL NAMES: the panel reload path re-derived symbol/timeframe by splitting
the file path on its FIRST underscore - mis-parsing every symbol containing
one (OANDA-style EUR_USD and US_500 are in our own alias lists) and knowing
only three timeframes. It now stores the (symbol, period) Load() was called
with and reuses them verbatim. Path-hostile characters in broker symbols
("EUR/USD") are sanitized by a shared AltDataFileSymbol() used by the panel,
the fetcher and TunedPeriods, so a slash cannot route a write into an
unintended subfolder.
DOWNLOAD VALIDATION: every FRED-family fetch now enforces a per-series
plausibility band (VIX 1-200, yields -5..30, CPI index 20-1000, ...) -
StringToDouble on transport garbage returns 0.0, and one absurd value poisons
every change/percentile feature computed across it (the BatchNorm NaN-latch
incident came from exactly one huge-but-finite input). Rejected rows are
counted and reported, never dropped silently.
LOUD EMPTINESS: a successful response with zero observations on an empty
cache now says so - naming the series (wrong id / format drift) or the COT
predicate (the unverified like-clauses) instead of leaving 0-filled features
unexplained. GEX gains a truncation guard: a day-over-day contract-count
collapse >50% is the fingerprint of a partial 13 MB download, not of markets,
and is skipped rather than recorded as a plausible-but-wrong number.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 17:59:10 -04:00
|
|
|
string daily = "Warrior_EA\\AltData\\" + fileSym + "_D1.csv";
|
2026-08-16 17:53:41 -04:00
|
|
|
if(FileIsExist(daily, FILE_COMMON))
|
|
|
|
|
{
|
|
|
|
|
PrintFormat("AltData: no %s - using the daily file %s (as-of join makes daily "
|
|
|
|
|
"alt rows valid on any timeframe).", name, daily);
|
|
|
|
|
name = daily;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-16 13:39:00 -04:00
|
|
|
if(!FileIsExist(name, FILE_COMMON))
|
|
|
|
|
{
|
|
|
|
|
if(!m_loggedMissing)
|
|
|
|
|
{
|
|
|
|
|
m_loggedMissing = true;
|
|
|
|
|
PrintFormat("AltData: no %s - external block %s.", name,
|
|
|
|
|
m_pinnedCount > 0 ? "0-fills its pinned width" : "disabled (0 features)");
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
//--- share flags unconditionally: the exporter may rewrite the file while charts hold it open
|
|
|
|
|
//--- (and the tester agent holds files, see the FileCopy share-flag bug).
|
|
|
|
|
int h = FileOpen(name, FILE_READ | FILE_TXT | FILE_ANSI | FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_COMMON);
|
|
|
|
|
if(h == INVALID_HANDLE)
|
|
|
|
|
{
|
|
|
|
|
PrintFormat("AltData: %s exists but FileOpen failed (%d).", name, GetLastError());
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
string header = FileReadString(h);
|
|
|
|
|
string cols[];
|
|
|
|
|
int nCols = StringSplit(header, ';', cols);
|
|
|
|
|
if(nCols < 2 || cols[0] != "date")
|
|
|
|
|
{
|
|
|
|
|
PrintFormat("AltData: %s header unrecognised ('%s') - ignoring file.", name, header);
|
|
|
|
|
FileClose(h);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
int fileCount = MathMin(nCols - 1, ALTDATA_MAX_FEATURES);
|
2026-08-25 22:51:50 -04:00
|
|
|
//--- LOCAL until the parse actually succeeds (rows>0 below) - m_fileNames used to be
|
|
|
|
|
//--- overwritten here unconditionally, before the row loop and before the rows==0 failure
|
|
|
|
|
//--- return, leaving it describing the NEW file's columns while m_fileCount/m_rows/m_vals
|
|
|
|
|
//--- still described the OLD one on a failed re-load (EnsureFresh() calls this live, while the
|
|
|
|
|
//--- exporter may be mid-rewrite of the CSV).
|
|
|
|
|
string newFileNames[];
|
|
|
|
|
ArrayResize(newFileNames, fileCount);
|
2026-08-16 13:39:00 -04:00
|
|
|
for(int c = 0; c < fileCount; c++)
|
2026-08-25 22:51:50 -04:00
|
|
|
newFileNames[c] = cols[c + 1];
|
2026-08-16 13:39:00 -04:00
|
|
|
int cap = 8192, rows = 0;
|
|
|
|
|
ArrayResize(m_rowTime, cap);
|
|
|
|
|
ArrayResize(m_vals, cap * fileCount);
|
|
|
|
|
datetime prev = 0;
|
|
|
|
|
while(!FileIsEnding(h))
|
|
|
|
|
{
|
|
|
|
|
string line = FileReadString(h);
|
|
|
|
|
if(StringLen(line) < 11)
|
|
|
|
|
continue;
|
|
|
|
|
string f[];
|
|
|
|
|
if(StringSplit(line, ';', f) < fileCount + 1)
|
|
|
|
|
continue;
|
|
|
|
|
datetime t = StringToTime(f[0]);
|
|
|
|
|
if(t <= 0 || t <= prev) // strictly ascending or the binary search breaks
|
|
|
|
|
continue;
|
|
|
|
|
if(rows >= cap)
|
|
|
|
|
{
|
|
|
|
|
cap = cap * 3 / 2;
|
|
|
|
|
ArrayResize(m_rowTime, cap);
|
|
|
|
|
ArrayResize(m_vals, cap * fileCount);
|
|
|
|
|
}
|
|
|
|
|
m_rowTime[rows] = t;
|
|
|
|
|
for(int c = 0; c < fileCount; c++)
|
|
|
|
|
{
|
|
|
|
|
string cell = f[c + 1];
|
|
|
|
|
m_vals[rows * fileCount + c] = (StringLen(cell) == 0) ? EMPTY_VALUE : StringToDouble(cell);
|
|
|
|
|
}
|
|
|
|
|
prev = t;
|
|
|
|
|
rows++;
|
|
|
|
|
}
|
|
|
|
|
FileClose(h);
|
|
|
|
|
if(rows == 0)
|
|
|
|
|
{
|
|
|
|
|
PrintFormat("AltData: %s parsed to 0 rows - ignoring file.", name);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
m_rows = rows;
|
|
|
|
|
m_fileCount = fileCount;
|
2026-08-25 22:51:50 -04:00
|
|
|
ArrayCopy(m_fileNames, newFileNames);
|
2026-08-16 13:39:00 -04:00
|
|
|
m_loadedFile = name;
|
fix(altdata): robustness pass for arbitrary symbol/timeframe - validation + error handling
Systematic audit of the alt-data stack against "any symbol, any timeframe",
prompted by the H4 surprise. Findings, each fixed:
CAPACITY: ALTDATA_MAX_FEATURES was 16 with FX symbols already at 15 - the next
added column would have been silently truncated by a MathMin. Raised to 32,
pin-chars 512 -> 1024.
SYMBOL NAMES: the panel reload path re-derived symbol/timeframe by splitting
the file path on its FIRST underscore - mis-parsing every symbol containing
one (OANDA-style EUR_USD and US_500 are in our own alias lists) and knowing
only three timeframes. It now stores the (symbol, period) Load() was called
with and reuses them verbatim. Path-hostile characters in broker symbols
("EUR/USD") are sanitized by a shared AltDataFileSymbol() used by the panel,
the fetcher and TunedPeriods, so a slash cannot route a write into an
unintended subfolder.
DOWNLOAD VALIDATION: every FRED-family fetch now enforces a per-series
plausibility band (VIX 1-200, yields -5..30, CPI index 20-1000, ...) -
StringToDouble on transport garbage returns 0.0, and one absurd value poisons
every change/percentile feature computed across it (the BatchNorm NaN-latch
incident came from exactly one huge-but-finite input). Rejected rows are
counted and reported, never dropped silently.
LOUD EMPTINESS: a successful response with zero observations on an empty
cache now says so - naming the series (wrong id / format drift) or the COT
predicate (the unverified like-clauses) instead of leaving 0-filled features
unexplained. GEX gains a truncation guard: a day-over-day contract-count
collapse >50% is the fingerprint of a partial 13 MB download, not of markets,
and is skipped rather than recorded as a plausible-but-wrong number.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 17:59:10 -04:00
|
|
|
m_loadedSymbol = symbol;
|
|
|
|
|
m_loadedPeriod = period;
|
2026-08-16 13:39:00 -04:00
|
|
|
ArrayResize(m_rowTime, rows);
|
|
|
|
|
ArrayResize(m_vals, rows * fileCount);
|
fix(altdata): median-fill instead of zero-fill, and a one-shot feature-vector autopsy
ALT-DATA AUDIT. The files themselves are healthy - all six symbols, 6,073 daily
rows, 2010-01-01 to 2026-08-17, no constant or degenerate columns, sane tails
(mac_cpi/mac_unemp flat ~47d is monthly data behaving correctly). The problem is
not the data, it is what happens where the data ISN'T.
CAltDataPanel::Features() returned an all-ZERO vector for any bar older than the
file's first row, and left blank cells at 0 too. Both were deliberate ('the block
is additive context and must degrade, never reject the bar') and that reasoning
holds for the CHANGE columns - but half these features are LEVELS: vix, ivol,
mac_y10, mac_cpi, mac_unemp, eia_util. For a level, 0 is not a missing reading,
it is an impossible one far outside the series' range. VIX does not visit zero.
And the spike lands in exactly the wrong place. Every alt file starts 2010-01-01
while the charts run far deeper - USDJPY H4 reaches ~1994, roughly HALF its
history - so 'alt block is all zeros' is precisely the predicate 'this bar is
older than 2010'. The IS/OOS split is chronological, so that predicate covers
~half of IS and none of OOS: an in-sample feature guaranteed to be useless
out-of-sample, and a bimodal input for the first BatchNorm to normalise. Not a
lookahead leak - a distribution corruption, which is quieter and was never
reported anywhere.
Now filled with the column MEDIAN over the covered range. A constant cannot leak
whatever its source - it takes the same value on every pre-coverage bar, so it
carries no information about which of those bars won - which is what makes a
median computed over later data legitimate here. Median not mean because the
series are skewed. Blank cells get the same treatment (eia_stk_idx1y alone has
181 blanks in 6,073 rows) and the count is now logged at load.
THE BACKOFF WAS ALREADY THERE AND WAS DEAD. Training.mqh arms m_coldSweepTick on
m_featureFailTransient, but only the open/ATR guards ever set that flag, so
f0cf659's cold ADMovingAverage looked PERMANENT and the sweep re-ran at full
speed forever. Setting the flag in the indicator guards revives the mechanism
that was already designed for this; no second backoff was needed and the one I
first wrote has been removed in favour of it.
SELF-HEALING, as asked. ReportFeatureHealth() runs once, the first time pass 1
produces usable windows, samples ~400 bars spread across the whole training range
and names every feature slot that is CONSTANT or mostly-zero, tagging alt-block
slots as alt[i]. Both of today's failures were the same shape - a block silently
produces nothing while every downstream number stays plausible - and neither an
accuracy figure nor a model can tell 'this feature is always 0' from 'this
feature is genuinely 0 here'. Evenly spaced sampling so a block that dies only in
deep history is caught as surely as one dead everywhere. A report, not a gate:
a rare-flag feature can be legitimately constant, and refusing to train would
turn a diagnostic into an outage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 12:53:51 -04:00
|
|
|
//--- Column medians over the covered range, ignoring blank cells - see m_median. Cheap: a few
|
|
|
|
|
//--- thousand rows x ~15 columns, once per file load.
|
|
|
|
|
ArrayResize(m_median, fileCount);
|
|
|
|
|
ArrayInitialize(m_median, 0.0);
|
|
|
|
|
int blanks = 0;
|
|
|
|
|
for(int c = 0; c < fileCount; c++)
|
|
|
|
|
{
|
|
|
|
|
double col[];
|
|
|
|
|
ArrayResize(col, rows);
|
|
|
|
|
int m = 0;
|
|
|
|
|
for(int rr = 0; rr < rows; rr++)
|
|
|
|
|
{
|
|
|
|
|
double v = m_vals[rr * fileCount + c];
|
|
|
|
|
if(v != EMPTY_VALUE)
|
|
|
|
|
col[m++] = v;
|
|
|
|
|
else
|
|
|
|
|
blanks++;
|
|
|
|
|
}
|
|
|
|
|
if(m <= 0)
|
|
|
|
|
continue;
|
|
|
|
|
ArrayResize(col, m);
|
refactor(stdlib): one quantile definition, from Math\Stat
The codebase had THREE conventions for the same statistic. AltData took a
true median; the barrier horizon and the derived input window took the
upper of the two middle values; the MI terciles and the barrier stop
ladder used nearest-rank indexing. All four now go through MathMedian /
MathQuantile, which is R's type 7 and the library's one answer.
System\AltData.mqh column median -> MathMedian (exact, no change)
AIBase\Labels.mqh swing median -> MathMedian
leg-range med -> MathMedian
stop ladder -> MathQuantile, read in one call
AIBase\Topology.mqh window median -> MathMedian
AIBase\AutoTune.mqh MI terciles -> MathQuantile + MathMin/MathMax
Signals\SignalSessionFilter DST last Sunday-> CDateTime::DaysInMonth()
gaps[]/legs[] change from int to double so MathMedian can read them; the
values are bar counts either way.
VALUES MOVE. Even-sample medians shift by half a bin and the quantile
reads interpolate, so the barrier geometry and the derived input window
can land on different rungs - re-keying fingerprints and forcing a
retrain. Accepted deliberately: stdlib consistency was the ask, and three
private conventions for one statistic is what it buys out.
Two YAGNI finds fell out of the ladder rewrite. MathQuantile sorts its own
copy, so DeriveBarrierGeometry no longer sorts up[]/dn[] in place - which
means upUnsorted[], a full array copy kept only to undo that sort, is
gone. ArraySort(up) had no consumer needing order at all; it was pure
work. The library call also gets a failure guard the hand-rolled indexing
never needed but the ladder read does.
Verified while here: Math\Stat\Math.mqh's MathAbs/MathMax/MathSqrt/MathPow
and friends are ARRAY overloads, not scalar redefinitions, so pulling it
into the translation unit shadows no builtin.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 20:16:03 -04:00
|
|
|
m_median[c] = MathMedian(col);
|
fix(altdata): median-fill instead of zero-fill, and a one-shot feature-vector autopsy
ALT-DATA AUDIT. The files themselves are healthy - all six symbols, 6,073 daily
rows, 2010-01-01 to 2026-08-17, no constant or degenerate columns, sane tails
(mac_cpi/mac_unemp flat ~47d is monthly data behaving correctly). The problem is
not the data, it is what happens where the data ISN'T.
CAltDataPanel::Features() returned an all-ZERO vector for any bar older than the
file's first row, and left blank cells at 0 too. Both were deliberate ('the block
is additive context and must degrade, never reject the bar') and that reasoning
holds for the CHANGE columns - but half these features are LEVELS: vix, ivol,
mac_y10, mac_cpi, mac_unemp, eia_util. For a level, 0 is not a missing reading,
it is an impossible one far outside the series' range. VIX does not visit zero.
And the spike lands in exactly the wrong place. Every alt file starts 2010-01-01
while the charts run far deeper - USDJPY H4 reaches ~1994, roughly HALF its
history - so 'alt block is all zeros' is precisely the predicate 'this bar is
older than 2010'. The IS/OOS split is chronological, so that predicate covers
~half of IS and none of OOS: an in-sample feature guaranteed to be useless
out-of-sample, and a bimodal input for the first BatchNorm to normalise. Not a
lookahead leak - a distribution corruption, which is quieter and was never
reported anywhere.
Now filled with the column MEDIAN over the covered range. A constant cannot leak
whatever its source - it takes the same value on every pre-coverage bar, so it
carries no information about which of those bars won - which is what makes a
median computed over later data legitimate here. Median not mean because the
series are skewed. Blank cells get the same treatment (eia_stk_idx1y alone has
181 blanks in 6,073 rows) and the count is now logged at load.
THE BACKOFF WAS ALREADY THERE AND WAS DEAD. Training.mqh arms m_coldSweepTick on
m_featureFailTransient, but only the open/ATR guards ever set that flag, so
f0cf659's cold ADMovingAverage looked PERMANENT and the sweep re-ran at full
speed forever. Setting the flag in the indicator guards revives the mechanism
that was already designed for this; no second backoff was needed and the one I
first wrote has been removed in favour of it.
SELF-HEALING, as asked. ReportFeatureHealth() runs once, the first time pass 1
produces usable windows, samples ~400 bars spread across the whole training range
and names every feature slot that is CONSTANT or mostly-zero, tagging alt-block
slots as alt[i]. Both of today's failures were the same shape - a block silently
produces nothing while every downstream number stays plausible - and neither an
accuracy figure nor a model can tell 'this feature is always 0' from 'this
feature is genuinely 0 here'. Evenly spaced sampling so a block that dies only in
deep history is caught as surely as one dead everywhere. A report, not a gate:
a rare-flag feature can be legitimately constant, and refusing to train would
turn a diagnostic into an outage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 12:53:51 -04:00
|
|
|
}
|
|
|
|
|
if(blanks > 0)
|
|
|
|
|
PrintFormat("AltData: %s has %d blank cells across %d rows x %d columns - these read as the"
|
|
|
|
|
" column MEDIAN, not as 0 (see m_median). A level series reported as 0 is an"
|
|
|
|
|
" impossible value, not a missing one.", name, blanks, rows, fileCount);
|
2026-08-16 13:39:00 -04:00
|
|
|
BuildMap();
|
|
|
|
|
PrintFormat("AltData: %s - %d features [%s], %d daily rows %s -> %s%s.",
|
|
|
|
|
name, FeatureCount(), NamesCsv(), rows,
|
|
|
|
|
TimeToString(m_rowTime[0], TIME_DATE), TimeToString(LastDate(), TIME_DATE),
|
|
|
|
|
m_pinnedCount > 0 ? " (pinned name list applied)" : "");
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--- As-of read: the last row dated <= t, mapped through the pin. Always fills FeatureCount()
|
|
|
|
|
//--- slots; anything unavailable is 0.0 (the neutral the sanitize loop would produce anyway).
|
|
|
|
|
void Features(datetime t, double &out[])
|
|
|
|
|
{
|
|
|
|
|
int n = FeatureCount();
|
|
|
|
|
ArrayResize(out, n);
|
|
|
|
|
ArrayInitialize(out, 0.0);
|
fix(altdata): median-fill instead of zero-fill, and a one-shot feature-vector autopsy
ALT-DATA AUDIT. The files themselves are healthy - all six symbols, 6,073 daily
rows, 2010-01-01 to 2026-08-17, no constant or degenerate columns, sane tails
(mac_cpi/mac_unemp flat ~47d is monthly data behaving correctly). The problem is
not the data, it is what happens where the data ISN'T.
CAltDataPanel::Features() returned an all-ZERO vector for any bar older than the
file's first row, and left blank cells at 0 too. Both were deliberate ('the block
is additive context and must degrade, never reject the bar') and that reasoning
holds for the CHANGE columns - but half these features are LEVELS: vix, ivol,
mac_y10, mac_cpi, mac_unemp, eia_util. For a level, 0 is not a missing reading,
it is an impossible one far outside the series' range. VIX does not visit zero.
And the spike lands in exactly the wrong place. Every alt file starts 2010-01-01
while the charts run far deeper - USDJPY H4 reaches ~1994, roughly HALF its
history - so 'alt block is all zeros' is precisely the predicate 'this bar is
older than 2010'. The IS/OOS split is chronological, so that predicate covers
~half of IS and none of OOS: an in-sample feature guaranteed to be useless
out-of-sample, and a bimodal input for the first BatchNorm to normalise. Not a
lookahead leak - a distribution corruption, which is quieter and was never
reported anywhere.
Now filled with the column MEDIAN over the covered range. A constant cannot leak
whatever its source - it takes the same value on every pre-coverage bar, so it
carries no information about which of those bars won - which is what makes a
median computed over later data legitimate here. Median not mean because the
series are skewed. Blank cells get the same treatment (eia_stk_idx1y alone has
181 blanks in 6,073 rows) and the count is now logged at load.
THE BACKOFF WAS ALREADY THERE AND WAS DEAD. Training.mqh arms m_coldSweepTick on
m_featureFailTransient, but only the open/ATR guards ever set that flag, so
f0cf659's cold ADMovingAverage looked PERMANENT and the sweep re-ran at full
speed forever. Setting the flag in the indicator guards revives the mechanism
that was already designed for this; no second backoff was needed and the one I
first wrote has been removed in favour of it.
SELF-HEALING, as asked. ReportFeatureHealth() runs once, the first time pass 1
produces usable windows, samples ~400 bars spread across the whole training range
and names every feature slot that is CONSTANT or mostly-zero, tagging alt-block
slots as alt[i]. Both of today's failures were the same shape - a block silently
produces nothing while every downstream number stays plausible - and neither an
accuracy figure nor a model can tell 'this feature is always 0' from 'this
feature is genuinely 0 here'. Evenly spaced sampling so a block that dies only in
deep history is caught as surely as one dead everywhere. A report, not a gate:
a rare-flag feature can be legitimately constant, and refusing to train would
turn a diagnostic into an outage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 12:53:51 -04:00
|
|
|
if(m_rows == 0 || n == 0)
|
2026-08-16 13:39:00 -04:00
|
|
|
return;
|
fix(altdata): median-fill instead of zero-fill, and a one-shot feature-vector autopsy
ALT-DATA AUDIT. The files themselves are healthy - all six symbols, 6,073 daily
rows, 2010-01-01 to 2026-08-17, no constant or degenerate columns, sane tails
(mac_cpi/mac_unemp flat ~47d is monthly data behaving correctly). The problem is
not the data, it is what happens where the data ISN'T.
CAltDataPanel::Features() returned an all-ZERO vector for any bar older than the
file's first row, and left blank cells at 0 too. Both were deliberate ('the block
is additive context and must degrade, never reject the bar') and that reasoning
holds for the CHANGE columns - but half these features are LEVELS: vix, ivol,
mac_y10, mac_cpi, mac_unemp, eia_util. For a level, 0 is not a missing reading,
it is an impossible one far outside the series' range. VIX does not visit zero.
And the spike lands in exactly the wrong place. Every alt file starts 2010-01-01
while the charts run far deeper - USDJPY H4 reaches ~1994, roughly HALF its
history - so 'alt block is all zeros' is precisely the predicate 'this bar is
older than 2010'. The IS/OOS split is chronological, so that predicate covers
~half of IS and none of OOS: an in-sample feature guaranteed to be useless
out-of-sample, and a bimodal input for the first BatchNorm to normalise. Not a
lookahead leak - a distribution corruption, which is quieter and was never
reported anywhere.
Now filled with the column MEDIAN over the covered range. A constant cannot leak
whatever its source - it takes the same value on every pre-coverage bar, so it
carries no information about which of those bars won - which is what makes a
median computed over later data legitimate here. Median not mean because the
series are skewed. Blank cells get the same treatment (eia_stk_idx1y alone has
181 blanks in 6,073 rows) and the count is now logged at load.
THE BACKOFF WAS ALREADY THERE AND WAS DEAD. Training.mqh arms m_coldSweepTick on
m_featureFailTransient, but only the open/ATR guards ever set that flag, so
f0cf659's cold ADMovingAverage looked PERMANENT and the sweep re-ran at full
speed forever. Setting the flag in the indicator guards revives the mechanism
that was already designed for this; no second backoff was needed and the one I
first wrote has been removed in favour of it.
SELF-HEALING, as asked. ReportFeatureHealth() runs once, the first time pass 1
produces usable windows, samples ~400 bars spread across the whole training range
and names every feature slot that is CONSTANT or mostly-zero, tagging alt-block
slots as alt[i]. Both of today's failures were the same shape - a block silently
produces nothing while every downstream number stays plausible - and neither an
accuracy figure nor a model can tell 'this feature is always 0' from 'this
feature is genuinely 0 here'. Evenly spaced sampling so a block that dies only in
deep history is caught as surely as one dead everywhere. A report, not a gate:
a rare-flag feature can be legitimately constant, and refusing to train would
turn a diagnostic into an outage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 12:53:51 -04:00
|
|
|
//--- BEFORE THE FILE'S FIRST ROW: median-fill, do not zero-fill. See m_median for why a zero here
|
|
|
|
|
//--- was a spike at an impossible value that coincided exactly with the chronological IS/OOS
|
|
|
|
|
//--- split. Still never rejects the bar - the block stays additive context, as designed.
|
|
|
|
|
if(t < m_rowTime[0])
|
|
|
|
|
{
|
|
|
|
|
for(int j = 0; j < n; j++)
|
|
|
|
|
{
|
|
|
|
|
int c = m_map[j];
|
|
|
|
|
if(c >= 0 && c < ArraySize(m_median))
|
|
|
|
|
out[j] = m_median[c];
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-08-21 15:01:50 -04:00
|
|
|
//--- ROW DATES ARE NAIVE, BAR TIMES ARE BROKER TIME. A row dated D means "public from D 00:00
|
2026-08-22 00:25:52 -04:00
|
|
|
//--- UTC", so the first bar of each broker day reads it the broker's offset (2-3h) early.
|
|
|
|
|
//--- Shorten a stamp and this becomes a leak: the buffer is the guarantee, not this
|
|
|
|
|
//--- comparison.
|
2026-08-16 13:39:00 -04:00
|
|
|
int lo = 0, hi = m_rows - 1;
|
|
|
|
|
while(lo < hi)
|
|
|
|
|
{
|
|
|
|
|
int mid = (lo + hi + 1) / 2;
|
|
|
|
|
if(m_rowTime[mid] <= t)
|
|
|
|
|
lo = mid;
|
|
|
|
|
else
|
|
|
|
|
hi = mid - 1;
|
|
|
|
|
}
|
|
|
|
|
int base = lo * m_fileCount;
|
|
|
|
|
for(int j = 0; j < n; j++)
|
|
|
|
|
{
|
|
|
|
|
int c = m_map[j];
|
|
|
|
|
if(c < 0)
|
|
|
|
|
continue;
|
|
|
|
|
double v = m_vals[base + c];
|
fix(altdata): median-fill instead of zero-fill, and a one-shot feature-vector autopsy
ALT-DATA AUDIT. The files themselves are healthy - all six symbols, 6,073 daily
rows, 2010-01-01 to 2026-08-17, no constant or degenerate columns, sane tails
(mac_cpi/mac_unemp flat ~47d is monthly data behaving correctly). The problem is
not the data, it is what happens where the data ISN'T.
CAltDataPanel::Features() returned an all-ZERO vector for any bar older than the
file's first row, and left blank cells at 0 too. Both were deliberate ('the block
is additive context and must degrade, never reject the bar') and that reasoning
holds for the CHANGE columns - but half these features are LEVELS: vix, ivol,
mac_y10, mac_cpi, mac_unemp, eia_util. For a level, 0 is not a missing reading,
it is an impossible one far outside the series' range. VIX does not visit zero.
And the spike lands in exactly the wrong place. Every alt file starts 2010-01-01
while the charts run far deeper - USDJPY H4 reaches ~1994, roughly HALF its
history - so 'alt block is all zeros' is precisely the predicate 'this bar is
older than 2010'. The IS/OOS split is chronological, so that predicate covers
~half of IS and none of OOS: an in-sample feature guaranteed to be useless
out-of-sample, and a bimodal input for the first BatchNorm to normalise. Not a
lookahead leak - a distribution corruption, which is quieter and was never
reported anywhere.
Now filled with the column MEDIAN over the covered range. A constant cannot leak
whatever its source - it takes the same value on every pre-coverage bar, so it
carries no information about which of those bars won - which is what makes a
median computed over later data legitimate here. Median not mean because the
series are skewed. Blank cells get the same treatment (eia_stk_idx1y alone has
181 blanks in 6,073 rows) and the count is now logged at load.
THE BACKOFF WAS ALREADY THERE AND WAS DEAD. Training.mqh arms m_coldSweepTick on
m_featureFailTransient, but only the open/ATR guards ever set that flag, so
f0cf659's cold ADMovingAverage looked PERMANENT and the sweep re-ran at full
speed forever. Setting the flag in the indicator guards revives the mechanism
that was already designed for this; no second backoff was needed and the one I
first wrote has been removed in favour of it.
SELF-HEALING, as asked. ReportFeatureHealth() runs once, the first time pass 1
produces usable windows, samples ~400 bars spread across the whole training range
and names every feature slot that is CONSTANT or mostly-zero, tagging alt-block
slots as alt[i]. Both of today's failures were the same shape - a block silently
produces nothing while every downstream number stays plausible - and neither an
accuracy figure nor a model can tell 'this feature is always 0' from 'this
feature is genuinely 0 here'. Evenly spaced sampling so a block that dies only in
deep history is caught as surely as one dead everywhere. A report, not a gate:
a rare-flag feature can be legitimately constant, and refusing to train would
turn a diagnostic into an outage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 12:53:51 -04:00
|
|
|
//--- A BLANK CELL is a gap in the collector's series, not a reading of zero - measured
|
|
|
|
|
//--- 2026-08-17: eia_stk_idx1y alone has 181 blanks in 6,073 rows. Same argument as the
|
|
|
|
|
//--- pre-coverage fill above, so the same constant.
|
|
|
|
|
out[j] = (v != EMPTY_VALUE) ? v
|
|
|
|
|
: ((c < ArraySize(m_median)) ? m_median[c] : 0.0);
|
2026-08-16 13:39:00 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//--- Live freshness: the exporter rewrites the file on its own schedule; reload when the chart
|
|
|
|
|
//--- has moved past the loaded data, at most once an hour. In the tester the newest bar is
|
|
|
|
|
//--- historical and always <= LastDate(), so this never fires there - by design.
|
|
|
|
|
void EnsureFresh(datetime newestBar)
|
|
|
|
|
{
|
|
|
|
|
if(m_rows == 0 || newestBar <= LastDate() + 86400)
|
|
|
|
|
return;
|
|
|
|
|
if(TimeCurrent() - m_lastLoadTry < 3600)
|
|
|
|
|
return;
|
|
|
|
|
string sym = m_loadedFile; // reload the exact file we came from
|
|
|
|
|
if(sym == "")
|
|
|
|
|
return;
|
|
|
|
|
PrintFormat("AltData: newest bar %s is beyond loaded data (%s) - reloading %s.",
|
|
|
|
|
TimeToString(newestBar, TIME_DATE), TimeToString(LastDate(), TIME_DATE), m_loadedFile);
|
|
|
|
|
//--- Re-parse in place; a failed reload keeps the old (stale but valid) panel.
|
|
|
|
|
m_lastLoadTry = TimeCurrent();
|
|
|
|
|
int h = FileOpen(m_loadedFile, FILE_READ | FILE_TXT | FILE_ANSI | FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_COMMON);
|
|
|
|
|
if(h == INVALID_HANDLE)
|
|
|
|
|
return;
|
|
|
|
|
FileClose(h);
|
2026-08-22 00:25:52 -04:00
|
|
|
//--- Cheap existence probe passed; reload with the EXACT (symbol, period) Load() was called
|
|
|
|
|
//--- with. Never reconstruct what you already know.
|
fix(altdata): robustness pass for arbitrary symbol/timeframe - validation + error handling
Systematic audit of the alt-data stack against "any symbol, any timeframe",
prompted by the H4 surprise. Findings, each fixed:
CAPACITY: ALTDATA_MAX_FEATURES was 16 with FX symbols already at 15 - the next
added column would have been silently truncated by a MathMin. Raised to 32,
pin-chars 512 -> 1024.
SYMBOL NAMES: the panel reload path re-derived symbol/timeframe by splitting
the file path on its FIRST underscore - mis-parsing every symbol containing
one (OANDA-style EUR_USD and US_500 are in our own alias lists) and knowing
only three timeframes. It now stores the (symbol, period) Load() was called
with and reuses them verbatim. Path-hostile characters in broker symbols
("EUR/USD") are sanitized by a shared AltDataFileSymbol() used by the panel,
the fetcher and TunedPeriods, so a slash cannot route a write into an
unintended subfolder.
DOWNLOAD VALIDATION: every FRED-family fetch now enforces a per-series
plausibility band (VIX 1-200, yields -5..30, CPI index 20-1000, ...) -
StringToDouble on transport garbage returns 0.0, and one absurd value poisons
every change/percentile feature computed across it (the BatchNorm NaN-latch
incident came from exactly one huge-but-finite input). Rejected rows are
counted and reported, never dropped silently.
LOUD EMPTINESS: a successful response with zero observations on an empty
cache now says so - naming the series (wrong id / format drift) or the COT
predicate (the unverified like-clauses) instead of leaving 0-filled features
unexplained. GEX gains a truncation guard: a day-over-day contract-count
collapse >50% is the fingerprint of a partial 13 MB download, not of markets,
and is skipped rather than recorded as a plausible-but-wrong number.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 17:59:10 -04:00
|
|
|
if(m_loadedSymbol != "")
|
|
|
|
|
Load(m_loadedSymbol, m_loadedPeriod);
|
2026-08-16 13:39:00 -04:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
#undef ALTDATA_MAX_FEATURES
|
2026-08-24 01:31:14 -04:00
|
|
|
#endif // WARRIOR_ALTDATA_MQH
|