//+------------------------------------------------------------------+ //| AltDataFeed.mqh | //| AnimateDread | //| | //| THE DAILY MACRO / POSITIONING BLOCK, read once at init. | //| | //| Common\Files\ADAltData\_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