Warrior_EA/Expert/AIBase/MetaCorpus.mqh

235 lines
10 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 |
//| one mapping S2 depends on: DB rows carry GMT timestamps while |
//| bar history is in server time, so the report tries hour offsets |
//| 0..+3 and prints the exact-bar match rate of each - measured, |
//| not assumed (the broker is UTC+2-ish; DST behaviour is exactly |
//| the kind of fact this project checks empirically). |
//+------------------------------------------------------------------+
#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
{
datetime gmt; // journal timestamp (GMT, 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;
TimeGMT(g);
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. ***");
}
//+------------------------------------------------------------------+