193 lines
7.9 KiB
MQL5
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
|