Warrior_EA/Expert/AIBase/MetaCorpus.mqh

237 lines
11 KiB
MQL5
Raw Permalink Normal View History

//+------------------------------------------------------------------+
//| Warrior_EA |
//| AnimateDread |
//| |
//| Meta-label training corpus - stage S1 of Meta_Labeling_Design.md |
//| |
//| The corpus is READ FROM THE SIGNAL DATABASE, not produced by a |
//| training-time re-implementation of the classic ladders. The DB |
//| logs every pattern instance the live ladders fire - both sides, |
//| uncensored, with the filter's net vote and the touchable entry |
//| price (per-side journaling, 652bf81/195be20) - so a corpus read |
//| from it is BY CONSTRUCTION identical to what fires live, and the |
//| several hundred lines of condition-mirroring a separate sweep |
//| would need (a silent-divergence trap) never get written. The |
//| cost of the trade: corpus coverage equals whatever backtest |
//| last populated the DB, so a corpus build IS a tester run with |
//| UseDatabaseRanking on and DB_MaxRowsPerTable raised. |
//| |
//| S1 ships the reader and its report; S2 attaches labels (triple- |
//| barrier at the EA's own geometry from the candidate's bar) and |
//| the setup-descriptor features. The report already measures the |
refactor(time): broker time throughout - and the GMT DB basis was already a live bug User decision: "stick to the broker's time throughout the codebase and analysis, session filter, programmed close time etc". Investigation found the GMT choice was not just inconsistent but broken: live journaling stamped DB rows with TimeGMT() while the online-learning backfill stamped them with BAR time (server) - two clocks ~3h apart in the same column. The newest-row duplicate guard compares them on one axis, so a live row landing within the offset after a backfill row was silently rejected as "outdated". dbVersion 3.0 -> 4.0 wipes the Signals store: the only honest reset for a mixed-basis corpus. - Direction()'s clock (stamps every journaled row, keys the per-second vote window): TimeGMT -> TimeCurrent, variables renamed so the name cannot lie about the basis. - UpdateSignalsWeights' future-row bound: same clock as the rows. - Session filter: broker-time anchors (London 10-18, NY 15-23:59, Tokyo 2-11). The GMT anchors were backwards for an EET-family broker - such a broker follows European DST, so London is DST-STABLE in broker time and moved twice a year in GMT. Tokyo drifts 1h each European summer (no DST to track) - accepted, smallest error on offer. Also fixed: inTimeInterval ignored its datetime parameter and called TimeGMT fresh - a dead parameter hiding a hardwired clock. - MetaCorpus/SignalMETA: rows pre-4.0 are GMT, broker since; the GMT->server offset scan is KEPT because it measures rather than assumes - it pins 0 on new corpora and still resolves old ones. - AltDataFetch deliberately stays on GMT: FRED/COT/EIA release schedules are external UTC-anchored events; the as-of join maps them onto server bars downstream. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 11:37:44 -04:00
//| one mapping S2 depends on: rows written BEFORE dbVersion 4.0 |
//| carry GMT timestamps while bar history is server time, so the |
//| report tries hour offsets 0..+3 and prints the exact-bar match |
//| rate of each - measured, not assumed. Since 4.0 (2026-08-19) new |
//| rows are stamped in BROKER time and the scan should pin offset 0;|
//| it is kept because it measures rather than assumes, so corpora |
//| from either era resolve correctly. |
//+------------------------------------------------------------------+
#include "..\..\Structures\tradeRecordStructure.mqh"
#include "..\..\System\PrintVerbose.mqh"
//--- One candidate = one journaled pattern instance. `win` is the DB's stop-and-reverse outcome
//--- (entry-to-reversal), kept for reporting; S2's training label is computed independently from
//--- price history (triple-barrier at the EA's geometry) and does NOT reuse this field.
struct SMetaCandidate
{
refactor(time): broker time throughout - and the GMT DB basis was already a live bug User decision: "stick to the broker's time throughout the codebase and analysis, session filter, programmed close time etc". Investigation found the GMT choice was not just inconsistent but broken: live journaling stamped DB rows with TimeGMT() while the online-learning backfill stamped them with BAR time (server) - two clocks ~3h apart in the same column. The newest-row duplicate guard compares them on one axis, so a live row landing within the offset after a backfill row was silently rejected as "outdated". dbVersion 3.0 -> 4.0 wipes the Signals store: the only honest reset for a mixed-basis corpus. - Direction()'s clock (stamps every journaled row, keys the per-second vote window): TimeGMT -> TimeCurrent, variables renamed so the name cannot lie about the basis. - UpdateSignalsWeights' future-row bound: same clock as the rows. - Session filter: broker-time anchors (London 10-18, NY 15-23:59, Tokyo 2-11). The GMT anchors were backwards for an EET-family broker - such a broker follows European DST, so London is DST-STABLE in broker time and moved twice a year in GMT. Tokyo drifts 1h each European summer (no DST to track) - accepted, smallest error on offer. Also fixed: inTimeInterval ignored its datetime parameter and called TimeGMT fresh - a dead parameter hiding a hardwired clock. - MetaCorpus/SignalMETA: rows pre-4.0 are GMT, broker since; the GMT->server offset scan is KEPT because it measures rather than assumes - it pins 0 on new corpora and still resolves old ones. - AltDataFetch deliberately stays on GMT: FRED/COT/EIA release schedules are external UTC-anchored events; the as-of join maps them onto server bars downstream. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 11:37:44 -04:00
datetime gmt; // journal timestamp (GMT pre-dbVersion-4.0, BROKER time since; minute resolution)
short family; // 0=MA 1=RSI 2=MACD 3=Ichimoku
short pattern; // Pattern_N index within the family
short side; // +1 Buy / -1 Sell
double netVote; // the filter's own vote margin when it fired (decision context)
double entryPrice; // touchable side of the spread at fire time
bool closed; // result != NA
bool win; // result == Profit (stop-and-reverse outcome, reporting only)
};
string MetaFamilyName(const int f)
{
switch(f)
{
case 0: return "MA";
case 1: return "RSI";
case 2: return "MACD";
case 3: return "Ichimoku";
}
return "";
}
int MetaFamilyPatterns(const int f)
{
switch(f)
{
case 0: return 4; // Signals\SignalMA.mqh m_pattern_count
case 1: return 4; // Signals\SignalRSI.mqh
case 2: return 6; // Signals\SignalMACD.mqh
case 3: return 12; // Signals\SignalIchimoku.mqh
}
return 0;
}
class CMetaCorpus
{
public:
SMetaCandidate m_rows[];
int m_count;
CMetaCorpus(void) : m_count(0) {}
//--- Pull every pattern table into m_rows. Missing tables (family disabled in the run that
//--- populated this DB) are skipped and counted, not treated as errors.
int Load(void)
{
m_count = 0;
ArrayResize(m_rows, 0);
if(!dbm.OpenDatabase())
{
Print(__FUNCTION__ + ": signal database is not available - no corpus.");
return 0;
}
int missing = 0;
TradeRecord rec;
for(int f = 0; f < 4; f++)
for(int p = 0; p < MetaFamilyPatterns(f); p++)
for(int s = 0; s < 2; s++)
{
string table = MetaFamilyName(f) + "_Pattern_" + IntegerToString(p) +
(s == 0 ? "_Buy" : "_Sell");
TradeRecord rows[];
if(!dbm.FetchTradeRecords(table, rec, rows))
{
missing++;
continue;
}
int base = m_count;
ArrayResize(m_rows, base + ArraySize(rows));
for(int i = 0; i < ArraySize(rows); i++)
{
MqlDateTime t;
t.year = rows[i].year;
t.mon = rows[i].month;
t.day = rows[i].day;
t.hour = rows[i].hour;
t.min = rows[i].minutes;
t.sec = 0;
m_rows[base + i].gmt = StructToTime(t);
m_rows[base + i].family = (short)f;
m_rows[base + i].pattern = (short)p;
m_rows[base + i].side = (short)(rows[i].direction == "Buy" ? 1 : -1);
m_rows[base + i].netVote = rows[i].netVote;
m_rows[base + i].entryPrice = rows[i].entryPrice;
m_rows[base + i].closed = (rows[i].result != "NA");
m_rows[base + i].win = (rows[i].result == "Profit");
}
m_count += ArraySize(rows);
}
if(missing > 0)
PrintVerbose(__FUNCTION__ + ": " + IntegerToString(missing) +
" pattern table(s) absent (family disabled in the populating run) - skipped.");
return m_count;
}
//--- Corpus state, printed for the log: volume per family, closed fraction, span, and the
//--- GMT->server-bar offset measurement S2's label plumbing will pin itself to.
void Report(void)
{
if(m_count == 0)
{
Print("MetaCorpus: EMPTY - run a backtest with UseDatabaseRanking=true (and "
"DB_MaxRowsPerTable raised for a long window) to build the corpus.");
return;
}
datetime lo = m_rows[0].gmt, hi = m_rows[0].gmt;
int famRows[4] = {0, 0, 0, 0}, famClosed[4] = {0, 0, 0, 0}, famWins[4] = {0, 0, 0, 0};
for(int i = 0; i < m_count; i++)
{
lo = MathMin(lo, m_rows[i].gmt);
hi = MathMax(hi, m_rows[i].gmt);
famRows[m_rows[i].family]++;
if(m_rows[i].closed)
{
famClosed[m_rows[i].family]++;
if(m_rows[i].win)
famWins[m_rows[i].family]++;
}
}
Print("MetaCorpus: ", m_count, " candidates, ", TimeToString(lo, TIME_DATE), " .. ",
TimeToString(hi, TIME_DATE));
//--- The cross-run trap, caught loudly: ProcessSignal's outdated-row guard rejects any
//--- registration OLDER than a row its table already holds (correct for a live stream,
//--- append-only-forward across runs). A tester run that starts before the DB's newest row
//--- will therefore silently register NOTHING for the overlap - an 18-year corpus build
//--- against leftover recent rows yields a corpus of only the leftover (measured 2026-08-12:
//--- 3,681 rows, all 2026, from exactly that mistake). Corpus builds start from an empty DB.
if(MQLInfoInteger(MQL_TESTER) && hi > TimeCurrent())
Print("MetaCorpus: WARNING - the DB already holds rows newer than this test's start (",
TimeToString(hi, TIME_DATE), " > ", TimeToString(TimeCurrent(), TIME_DATE),
"). Historical registrations will be REJECTED by the outdated-row guard; ",
"wipe the signal DB before a corpus-building backtest.");
for(int f = 0; f < 4; f++)
{
if(famRows[f] == 0)
continue;
double wr = famClosed[f] > 0 ? 100.0 * famWins[f] / famClosed[f] : 0.0;
Print(" ", MetaFamilyName(f), ": ", famRows[f], " rows, ", famClosed[f],
" closed, S&R win rate ", DoubleToString(wr, 1), "%");
}
//--- The offset measurement. A candidate is only labelable if its timestamp resolves to an
//--- exact bar; rows are journaled at bar opens, so the RIGHT offset should match ~100% and
//--- wrong ones near-0% on an H1 chart - a clean empirical signature. Measured on every row.
Print(" GMT->server bar match rate by hour offset (S2 pins to the winner):");
for(int off = 0; off <= 3; off++)
{
int hit = 0;
for(int i = 0; i < m_count; i++)
if(iBarShift(_Symbol, _Period, m_rows[i].gmt + off * 3600, true) >= 0)
hit++;
Print(" +", off, "h: ", DoubleToString(100.0 * hit / m_count, 1), "% exact");
}
}
};
//--- OnInit hook: load + report under VerboseMode, so a corpus-building backtest is verifiable
//--- straight from its log. Deliberately report-only in S1 - nothing downstream consumes it yet.
void MetaCorpusReport(void)
{
CMetaCorpus corpus;
corpus.Load();
corpus.Report();
}
//--- ALWAYS-ON tester guard, deliberately NOT VerboseMode-gated: the stale-DB warning inside
//--- Report() was, and forgetting the wipe silently wasted a full 18-year corpus run - twice.
//--- 52 one-row queries at OnInit; absent tables (family disabled, or a fresh wipe before
//--- AddFilter creates them) probe quietly as "no rows".
void MetaCorpusStaleCheck(void)
{
if(!MQLInfoInteger(MQL_TESTER))
return;
long newest = 0;
bool any = false;
for(int f = 0; f < 4; f++)
for(int p = 0; p < MetaFamilyPatterns(f); p++)
for(int s = 0; s < 2; s++)
{
string table = MetaFamilyName(f) + "_Pattern_" + IntegerToString(p) +
(s == 0 ? "_Buy" : "_Sell");
long k = 0;
bool found = false;
if(dbm.FetchNewestTimeKey(table, k, found, true) && found)
{
newest = MathMax(newest, k);
any = true;
}
}
if(!any)
return;
MqlDateTime g;
refactor(time): broker time throughout - and the GMT DB basis was already a live bug User decision: "stick to the broker's time throughout the codebase and analysis, session filter, programmed close time etc". Investigation found the GMT choice was not just inconsistent but broken: live journaling stamped DB rows with TimeGMT() while the online-learning backfill stamped them with BAR time (server) - two clocks ~3h apart in the same column. The newest-row duplicate guard compares them on one axis, so a live row landing within the offset after a backfill row was silently rejected as "outdated". dbVersion 3.0 -> 4.0 wipes the Signals store: the only honest reset for a mixed-basis corpus. - Direction()'s clock (stamps every journaled row, keys the per-second vote window): TimeGMT -> TimeCurrent, variables renamed so the name cannot lie about the basis. - UpdateSignalsWeights' future-row bound: same clock as the rows. - Session filter: broker-time anchors (London 10-18, NY 15-23:59, Tokyo 2-11). The GMT anchors were backwards for an EET-family broker - such a broker follows European DST, so London is DST-STABLE in broker time and moved twice a year in GMT. Tokyo drifts 1h each European summer (no DST to track) - accepted, smallest error on offer. Also fixed: inTimeInterval ignored its datetime parameter and called TimeGMT fresh - a dead parameter hiding a hardwired clock. - MetaCorpus/SignalMETA: rows pre-4.0 are GMT, broker since; the GMT->server offset scan is KEPT because it measures rather than assumes - it pins 0 on new corpora and still resolves old ones. - AltDataFetch deliberately stays on GMT: FRED/COT/EIA release schedules are external UTC-anchored events; the as-of join maps them onto server bars downstream. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 11:37:44 -04:00
TimeCurrent(g); // broker clock, matching the row stamps since dbVersion 4.0
long nowKey = SignalTimeKey(g.year, g.mon, g.day, g.hour, g.min);
if(newest >= nowKey)
Print("MetaCorpus: *** THE SIGNAL DB ALREADY HOLDS ROWS AT/AFTER THIS TEST'S START (key ",
newest, " >= ", nowKey, "). Every registration in the overlap will be REJECTED by the ",
"outdated-row guard and this run will build NO corpus. Stop the test, wipe the ",
"Signals folder, and rerun. ***");
}
//+------------------------------------------------------------------+