Warrior_EA/System/AltDataFeed.mqh

193 lines
7.9 KiB
MQL5

//+------------------------------------------------------------------+
//| AltDataFeed.mqh |
//| AnimateDread |
//| |
//| THE DAILY MACRO / POSITIONING BLOCK, read once at init. |
//| |
//| Common\Files\ADAltData\<symbol>_D1.csv - 6,096 rows, 2010-01-01 |
//| onward, semicolon separated. Sixteen usable columns: VIX and its |
//| 5-day change, the dollar's 5-day change, COT speculative net plus |
//| its 1y/3y percentiles and 4-week change, three EIA inventory |
//| series, and six macro series (10y, curve, breakeven, output gap, |
//| CPI, unemployment). |
//| |
//| ⚠ ivol AND ivol_chg5 ARE EMPTY IN EVERY ROW and are not loaded. |
//| Measured 2026-09-11: 0.0% filled on EURUSD, against ~100% for the |
//| other sixteen. Loading them would hand the network two constant |
//| zero columns, which is not neutral - it spends input width and |
//| lets the optimiser fit noise through the bias attached to them. |
//| |
//| WHY IT CANNOT LOOK AHEAD. A row dated D carries values known at |
//| D's close. Features are built from shift 1 - the last CLOSED bar |
//| - so the row matching that bar's date was fully observable before |
//| the bar being decided even opened. `Lookup` therefore takes the |
//| newest row whose date is <= the closed bar's date, never the row |
//| after it, and a missing date resolves BACKWARD (a holiday reads |
//| Friday's macro state, which is what a trader would have had). |
//| |
//| AND WHY IT IS NOT ZERO-FILLED. The series begin in 2010. A run |
//| starting earlier must SKIP those bars, not pad them: a column of |
//| zeros for 2005-2009 teaches the network the era rather than the |
//| market, and it would learn "alt data absent" as a tradeable |
//| state. Lookup() returns false and the caller drops the row. |
//+------------------------------------------------------------------+
#ifndef WARRIOR_ALTDATA_FEED_MQH
#define WARRIOR_ALTDATA_FEED_MQH
#define ALT_COLUMNS 16
class CAltDataFeed
{
private:
datetime m_date[]; // ascending, one per row
double m_val[]; // flat, ALT_COLUMNS per row
int m_rows;
string m_why;
string m_loadedFor;
//--- The CSV's own column order, minus the two empty ones. Index into the split line.
//--- 0=date 1=vix 2=vix_chg5 3=usd_chg5 [4=ivol 5=ivol_chg5 SKIPPED] 6=cot_spec_net
//--- 7=cot_idx_1y 8=cot_idx_3y 9=cot_chg_4w 10..12=eia 13..18=mac
static void SourceColumns(int &c[])
{
ArrayResize(c, ALT_COLUMNS);
int k = 0;
c[k++] = 1; c[k++] = 2; c[k++] = 3;
c[k++] = 6; c[k++] = 7; c[k++] = 8; c[k++] = 9;
c[k++] = 10; c[k++] = 11; c[k++] = 12;
c[k++] = 13; c[k++] = 14; c[k++] = 15; c[k++] = 16; c[k++] = 17; c[k++] = 18;
}
public:
CAltDataFeed(void) : m_rows(0), m_why("not loaded"), m_loadedFor("") {}
~CAltDataFeed(void) {}
bool Ok(void) const { return m_rows > 0; }
int Rows(void) const { return m_rows; }
string Why(void) const { return m_why; }
static string ColumnName(const int i);
bool Load(const string symbol);
//--- Values for the bar CLOSING at `when`. False when the date predates the series.
bool Lookup(const datetime when, double &out[]) const;
};
//+------------------------------------------------------------------+
string CAltDataFeed::ColumnName(const int i)
{
switch(i)
{
case 0: return "vix";
case 1: return "vix_chg5";
case 2: return "usd_chg5";
case 3: return "cot_spec_net";
case 4: return "cot_idx_1y";
case 5: return "cot_idx_3y";
case 6: return "cot_chg_4w";
case 7: return "eia_stk_idx1y";
case 8: return "eia_stk_chg4";
case 9: return "eia_util";
case 10: return "mac_y10";
case 11: return "mac_curve";
case 12: return "mac_bei";
case 13: return "mac_gap";
case 14: return "mac_cpi";
case 15: return "mac_unemp";
}
return "?";
}
//+------------------------------------------------------------------+
bool CAltDataFeed::Load(const string symbol)
{
if(m_loadedFor == symbol && m_rows > 0)
return true;
m_rows = 0;
ArrayResize(m_date, 0);
ArrayResize(m_val, 0);
const string path = "ADAltData\\" + symbol + "_D1.csv";
//--- FILE_COMMON: the corpus is shared by the tester and the live terminal, exactly like the
//--- journal database, so one file serves every agent rather than one copy per sandbox.
const int h = FileOpen(path, FILE_READ | FILE_TXT | FILE_ANSI | FILE_COMMON);
if(h == INVALID_HANDLE)
{
m_why = StringFormat("cannot open Common\\Files\\%s (%d)", path, GetLastError());
return false;
}
int src[];
SourceColumns(src);
string parts[];
bool header = true;
while(!FileIsEnding(h))
{
const string line = FileReadString(h);
if(header) // the column names, not data
{ header = false; continue; }
if(StringLen(line) < 10)
continue;
if(StringSplit(line, ';', parts) < 19)
continue;
//--- "2010-01-01" -> "2010.01.01"; StringToTime wants dots.
string d = parts[0];
StringReplace(d, "-", ".");
const datetime when = StringToTime(d);
if(when == 0)
continue;
const int r = m_rows;
ArrayResize(m_date, r + 1);
ArrayResize(m_val, (r + 1) * ALT_COLUMNS);
m_date[r] = when;
for(int c = 0; c < ALT_COLUMNS; c++)
{
const string cell = parts[src[c]];
//--- An empty cell carries the previous row's value forward - a macro series that has not
//--- printed yet has not CHANGED either, and the last print is what a trader would be
//--- acting on. Only the very first rows can have nothing to carry, and they read 0.
double v = 0.0;
if(StringLen(cell) > 0)
v = StringToDouble(cell);
else
if(r > 0)
v = m_val[(r - 1) * ALT_COLUMNS + c];
m_val[r * ALT_COLUMNS + c] = v;
}
m_rows++;
}
FileClose(h);
if(m_rows <= 0)
{
m_why = "file opened but no usable rows";
return false;
}
m_loadedFor = symbol;
m_why = "";
return true;
}
//+------------------------------------------------------------------+
bool CAltDataFeed::Lookup(const datetime when, double &out[]) const
{
ArrayResize(out, ALT_COLUMNS);
ArrayInitialize(out, 0.0);
if(m_rows <= 0 || when < m_date[0])
return false; // predates the series - the caller drops the row
//--- Newest row at or before `when`. Binary search: this runs once per training row, and a
//--- linear scan over 6,000 rows x 4,000 rows of training is 24M comparisons per fit.
int lo = 0, hi = m_rows - 1, best = -1;
while(lo <= hi)
{
const int mid = (lo + hi) / 2;
if(m_date[mid] <= when)
{ best = mid; lo = mid + 1; }
else
hi = mid - 1;
}
if(best < 0)
return false;
for(int c = 0; c < ALT_COLUMNS; c++)
out[c] = m_val[best * ALT_COLUMNS + c];
return true;
}
//+------------------------------------------------------------------+
//| ONE FEED PER CHART, like the Wyckoff one and for the same reason. |
//+------------------------------------------------------------------+
CAltDataFeed g_altData;
#endif // WARRIOR_ALTDATA_FEED_MQH