- 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
|