forked from mnbvc188199/Warrior_EA
- Replaced standard library signal modules with custom implementations to allow for named patterns and improved voting. - Added new input parameters for module weights, allowing for optimization of individual signal contributions. - Enhanced the management of trades with new options for breakeven and management cut. - Introduced a mechanism for dynamic ranking of signal weights based on historical performance. - Improved initialization logic to ensure proper registration of filters and handling of trading conditions. - Added detailed logging for trading permissions and account status during initialization.
279 lines
12 KiB
MQL5
279 lines
12 KiB
MQL5
//+------------------------------------------------------------------+
| |
//| DipMeta.mqh |
| |
//| AnimateDread |
| |
//| |
| |
//| THE META-LABEL FOR THE DIP-BUY: take this dip, or skip it. |
| |
//| |
| |
//| The architecture the operator settled on: the SETUP is the |
| |
//| strategy, the model only says yes or no to a trade the rules have |
| |
//| already found. The dip-buy is the first setup in this EA with a |
| |
//| measured positive base rate to say no against (60-77% wins, +10 |
| |
//| to +35 bp a trade), so it is the first place a model earns a seat.|
| |
//| |
| |
//| WHAT IT LEARNS FROM. Every past signal bar becomes one row: the |
| |
//| state of the market at that bar (depth of the dip, distance to |
| |
//| the 200-bar average, the streak, the bar's own shape, the vol |
| |
//| regime, ER/VR, weekday, hour) and the label is what the EA's OWN |
| |
//| exit rule would have made of it - stop, mean-cross or time stop, |
| |
//| simulated on closed bars. No journal, no file, no Python: the |
| |
//| rows are rebuilt from history-so-far on every refit, so the fit |
| |
//| is walk-forward by construction and every bar it votes on is out |
| |
//| of sample. |
| |
//| |
| |
//| TWO ALGLIB MODELS, ONE ANSWER. A random decision forest and the |
| |
//| MLP (CWarriorNet) are fitted on the same chronological split with |
| |
//| the same embargo; each reports its held-out AUC; the score is the |
| |
//| mean of whichever are ready. The forest exists because 300 daily |
| |
//| dips cannot support even a two-unit MLP at ten rows per weight, |
| |
//| and a forest degrades gracefully where the net refuses. ALGLIB's |
| |
//| statistics (Pearson / Spearman) print each feature's correlation |
| |
//| with the label first, so a fit that "works" can be read against |
| |
//| what it could possibly have used. |
| |
//| |
| |
//| NEVER SERIALISED. DFSerialize crashes on compressed forests and |
| |
//| MLPSerialize dies in CSerializer::Stop() - both already met here. |
| |
//+------------------------------------------------------------------+
| |
#ifndef WARRIOR_DIP_META_MQH
| |
#define WARRIOR_DIP_META_MQH
| |
| |
#include "WarriorNet.mqh"
| |
| |
#define DIPMETA_FEATURES 15
| |
#define DIPMETA_TREES 100 // forest size
| |
#define DIPMETA_R 0.66 // fraction of rows each tree sees (ALGLIB's r)
| |
#define DIPMETA_MIN_ROWS 120 // the forest's floor; the MLP sets its own (10 rows/weight)
| |
| |
class CDipMeta
| |
{
| |
private:
| |
CWarriorNet m_mlp;
| |
CDecisionForestShell m_df;
| |
bool m_mlpReady, m_dfReady;
| |
double m_dfAuc;
| |
int m_nIn;
| |
string m_why;
| |
| |
static double RankAUC(double &p[], int &l[], const int n);
| |
| |
public:
| |
CDipMeta(void) : m_mlpReady(false), m_dfReady(false), m_dfAuc(-1.0), m_nIn(0), m_why("not trained") {}
| |
~CDipMeta(void) {}
| |
| |
bool Ready(void) const { return m_mlpReady || m_dfReady; }
| |
string Why(void) const { return m_why; }
| |
double ForestAUC(void) const { return m_dfAuc; }
| |
double MlpAUC(void) const { return m_mlpReady ? m_mlp.AUC() : -1.0; }
| |
| |
static string FeatureName(const int i);
| |
bool Train(CMatrixDouble &xy, const int rows, const int embargo);
| |
//--- P(this dip pays), the mean of the ready models; <0 when none is.
| |
double Score(double &x[]);
| |
};
| |
//+------------------------------------------------------------------+
| |
string CDipMeta::FeatureName(const int i)
| |
{
| |
switch(i)
| |
{
| |
//--- THE DIP ITSELF
| |
case 0: return "z20"; // how deep, in standard deviations of the 20-bar window
| |
case 1: return "dist200_atr"; // where it sits against the 200-bar mean, in ATR
| |
case 2: return "ret1_atr"; // the signal bar's own move
| |
case 3: return "ret5_atr"; // the five-bar move that produced the dip
| |
case 4: return "streak_down"; // consecutive down closes
| |
case 5: return "pos20"; // position inside the 20-bar range, 0 = at the low
| |
//--- THE SIGNAL BAR'S SHAPE. A wide bar closing at its low is capitulation; closing off the
| |
//--- low is a bounce that already began - and in the real trades day-1 direction was the
| |
//--- strongest single split (90% win vs 53%).
| |
case 6: return "range_atr";
| |
case 7: return "close_in_bar";
| |
case 8: return "gap_atr"; // the open against the previous close
| |
//--- THE REGIME. The dip inside a trend paid +117 bp, in chop +33, in reversion +13.
| |
case 9: return "vol_rel"; // ATR14 / ATR100 - is this a volatility event
| |
case 10: return "er20";
| |
case 11: return "vr60";
| |
case 12: return "regime";
| |
//--- THE CALENDAR. Thu/Fri fills +57/+55 vs Tue +3; Mondays lose on the H4 ensemble.
| |
case 13: return "dow";
| |
case 14: return "hour";
| |
}
| |
return "?";
| |
}
| |
//+------------------------------------------------------------------+
| |
//| Mann-Whitney rank-sum AUC, ties averaged. Same arithmetic as the |
| |
//| net's own report, so the two models are scored identically. |
| |
//+------------------------------------------------------------------+
| |
double CDipMeta::RankAUC(double &p[], int &l[], const int n)
| |
{
| |
int pos = 0;
| |
for(int i = 0; i < n; i++)
| |
if(l[i] == 1)
| |
pos++;
| |
const int neg = n - pos;
| |
if(pos == 0 || neg == 0)
| |
return 0.5;
| |
int idx[];
| |
ArrayResize(idx, n);
| |
for(int i = 0; i < n; i++)
| |
idx[i] = i;
| |
for(int i = 1; i < n; i++)
| |
{
| |
const int k = idx[i];
| |
int j = i - 1;
| |
while(j >= 0 && p[idx[j]] > p[k])
| |
{ idx[j + 1] = idx[j]; j--; }
| |
idx[j + 1] = k;
| |
}
| |
double rankSum = 0.0;
| |
int i2 = 0;
| |
while(i2 < n)
| |
{
| |
int j2 = i2;
| |
while(j2 + 1 < n && p[idx[j2 + 1]] == p[idx[i2]])
| |
j2++;
| |
const double avgRank = 0.5 * ((i2 + 1) + (j2 + 1));
| |
for(int k2 = i2; k2 <= j2; k2++)
| |
if(l[idx[k2]] == 1)
| |
rankSum += avgRank;
| |
i2 = j2 + 1;
| |
}
| |
return (rankSum - 0.5 * pos * (pos + 1.0)) / ((double)pos * neg);
| |
}
| |
//+------------------------------------------------------------------+
| |
bool CDipMeta::Train(CMatrixDouble &xy, const int rows, const int embargo)
| |
{
| |
m_mlpReady = false;
| |
m_dfReady = false;
| |
m_nIn = DIPMETA_FEATURES;
| |
if(rows < DIPMETA_MIN_ROWS)
| |
{
| |
m_why = StringFormat("%d row(s) is below the %d-row floor - not trained", rows, DIPMETA_MIN_ROWS);
| |
return false;
| |
}
| |
string names[];
| |
ArrayResize(names, m_nIn);
| |
for(int f = 0; f < m_nIn; f++)
| |
names[f] = FeatureName(f);
| |
| |
//--- 1. WHAT IS THERE TO LEARN. Each feature against the label, Pearson and Spearman, over every
| |
//--- row. Printed BEFORE the fits so the AUCs below are read against the raw material.
| |
double lab[];
| |
ArrayResize(lab, rows);
| |
int posAll = 0;
| |
for(int r = 0; r < rows; r++)
| |
{
| |
lab[r] = xy.Get(r, m_nIn);
| |
if(lab[r] > 0.5)
| |
posAll++;
| |
}
| |
string corr = "";
| |
double col[];
| |
ArrayResize(col, rows);
| |
for(int f = 0; f < m_nIn; f++)
| |
{
| |
for(int r = 0; r < rows; r++)
| |
col[r] = xy.Get(r, f);
| |
const double rp = CAlglib::PearsonCorr2(col, lab, rows);
| |
const double rs = CAlglib::SpearmanCorr2(col, lab, rows);
| |
corr += StringFormat("%s%s %+.3f/%+.3f", (f ? ", " : ""), names[f], rp, rs);
| |
}
| |
PrintFormat("CDipMeta: %d row(s), %d paid (%.1f%%). Feature vs label, Pearson/Spearman: %s",
| |
rows, posAll, 100.0 * posAll / rows, corr);
| |
| |
//--- 2. THE SPLIT, identical for both models: the tail is validation, `embargo` rows before it
| |
//--- are dropped so no training row shares its outcome window with the first validation row.
| |
const int valRows = (int)MathMax(1, MathRound(rows * WARRIOR_NET_VAL_FRAC));
| |
const int gap = (embargo > 0 && embargo < rows / 4) ? embargo : 0;
| |
const int trnRows = rows - valRows - gap;
| |
if(trnRows < 40)
| |
{
| |
m_why = "training half too small after the split";
| |
return false;
| |
}
| |
CMatrixDouble trn(trnRows, m_nIn + 1);
| |
for(int r = 0; r < trnRows; r++)
| |
for(int c = 0; c <= m_nIn; c++)
| |
trn.Set(r, c, xy.Get(r, c));
| |
| |
//--- 3. THE FOREST.
| |
int info = 0;
| |
CDFReportShell rep;
| |
CAlglib::DFBuildRandomDecisionForest(trn, trnRows, m_nIn, 2, DIPMETA_TREES, DIPMETA_R, info, m_df, rep);
| |
if(info > 0)
| |
{
| |
double p[]; int l[];
| |
ArrayResize(p, valRows); ArrayResize(l, valRows);
| |
double x[], y[];
| |
ArrayResize(x, m_nIn);
| |
int hit = 0, pos = 0;
| |
for(int r = 0; r < valRows; r++)
| |
{
| |
const int src = trnRows + gap + r;
| |
for(int c = 0; c < m_nIn; c++)
| |
x[c] = xy.Get(src, c);
| |
CAlglib::DFProcess(m_df, x, y);
| |
p[r] = (ArraySize(y) > 1) ? y[1] : 0.0;
| |
l[r] = (int)MathRound(xy.Get(src, m_nIn));
| |
if(l[r] == 1) pos++;
| |
if((p[r] >= 0.5 ? 1 : 0) == l[r]) hit++;
| |
}
| |
m_dfAuc = RankAUC(p, l, valRows);
| |
//--- DOES THE TOP THIRD PAY MORE THAN THE BOTTOM THIRD? The AUC says whether the ranking has
| |
//--- information; this says whether acting on it would have changed the win rate.
| |
int order[];
| |
ArrayResize(order, valRows);
| |
for(int i = 0; i < valRows; i++) order[i] = i;
| |
for(int i = 1; i < valRows; i++)
| |
{
| |
const int k = order[i]; int j = i - 1;
| |
while(j >= 0 && p[order[j]] > p[k]) { order[j + 1] = order[j]; j--; }
| |
order[j + 1] = k;
| |
}
| |
const int third = valRows / 3;
| |
int loWin = 0, hiWin = 0;
| |
for(int i = 0; i < third; i++)
| |
{
| |
if(l[order[i]] == 1) loWin++;
| |
if(l[order[valRows - 1 - i]] == 1) hiWin++;
| |
}
| |
m_dfReady = true;
| |
PrintFormat("CDipMeta: FOREST (%d trees) trained on %d / embargo %d / validation %d rows - "
| |
"OUT-OF-SAMPLE AUC %.3f, accuracy %.1f%% against a %.1f%% base rate; win rate in the "
| |
"model's bottom third %.0f%% vs top third %.0f%% (n %d each).",
| |
DIPMETA_TREES, trnRows, gap, valRows, m_dfAuc, 100.0 * hit / valRows,
| |
100.0 * pos / valRows, third > 0 ? 100.0 * loWin / third : 0.0,
| |
third > 0 ? 100.0 * hiWin / third : 0.0, third);
| |
}
| |
else
| |
PrintFormat("CDipMeta: DFBuildRandomDecisionForest refused the dataset (info %d).", info);
| |
| |
//--- 4. THE MLP, on the same rows; it refuses on its own when the sample cannot carry it.
| |
m_mlpReady = m_mlp.Train(xy, rows, m_nIn, names, embargo);
| |
if(!m_mlpReady)
| |
PrintFormat("CDipMeta: MLP not fitted - %s.", m_mlp.Why());
| |
| |
m_why = Ready() ? "trained" : "no model could be fitted";
| |
return Ready();
| |
}
| |
//+------------------------------------------------------------------+
| |
double CDipMeta::Score(double &x[])
| |
{
| |
if(!Ready() || ArraySize(x) != m_nIn)
| |
return -1.0;
| |
double sum = 0.0;
| |
int n = 0;
| |
if(m_dfReady)
| |
{
| |
double y[];
| |
CAlglib::DFProcess(m_df, x, y);
| |
if(ArraySize(y) > 1)
| |
{ sum += y[1]; n++; }
| |
}
| |
if(m_mlpReady)
| |
{
| |
const double p = m_mlp.Score(x);
| |
if(p >= 0.0)
| |
{ sum += p; n++; }
| |
}
| |
return (n > 0) ? sum / n : -1.0;
| |
}
| |
#endif // WARRIOR_DIP_META_MQH
|