Warrior_EA/Signals/SignalDipBuy.mqh
AnimateDread 47a5ef338b Refactor Warrior EA: Integrate custom signal modules, enhance voting mechanism, and improve management features
- 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.
2026-09-13 14:32:40 -04:00

457 lines
21 KiB
MQL5

//+------------------------------------------------------------------+
//| SignalDipBuy.mqh |
//| AnimateDread |
//| |
//| THE ONE EDGE THE DATA HAS: buy the high-volatility low on a US |
//| equity index at the daily close, hold about a week. |
//| |
//| MEASURED, not authored. The honest daily state scan (states x |
//| hold, picked in-sample <2019, pooled out-of-sample against the |
//| instrument's own unconditional drift, cost and swap charged) was |
//| run on every instrument with data - ten Dukascopy series and the |
//| broker's own index history. One cell family survived, and it is |
//| the same on all three US indices: |
//| SP500 posxvol=0x3 L5 +103 bp OOS (t 3.1) +17..+24 over drift|
//| NAS100 r20xrsi=0x0 L5 +109 bp OOS (t 3.7) +11 over drift |
//| US30 r20xrsi=0x0 L5 +102 bp OOS (t 3.1) +18 over drift |
//| 20-day range LOW x high volatility, or bottom-quintile 20-day |
//| return x lowest RSI(2): long, hold five days. The named form is |
//| Connors' RSI(2)<10 above the 200-day, which the 176-cell swing |
//| screen found flat across its whole neighbourhood (thresholds |
//| 5-25, every exit, with or without the stop: all positive). |
//| |
//| WHERE IT DOES NOT WORK, so nobody points it there: DAX40 and |
//| UK100 (picks LOSE out of sample), every forex pair (null on all |
//| five), gold (drift, not selection). US indices, long only. A long |
//| bias on an index is a property of the instrument, not a flaw. |
//| |
//| HOW IT SHOULD BE RUN. This is not a vote member. At thirteen |
//| voters one filter at 100 scores 7.7%, below any threshold, so it |
//| must be the ONLY registered module (UseClassics, UseWyckoff, |
//| UseNeural, UseRegime all off) with Direction = DIR_LONG. Its |
//| ShortCondition is the EXIT ("close above the short average"), |
//| reached through ThresholdClose - it never opens a short. |
//| |
//| THE STOP MUST BE WIDE. Measured: a 0.5-ATR stop destroyed the |
//| edge (+2.8 bp, 101 of 152 stopped) - the dip runs THROUGH a tight |
//| stop before it reverts. 2-3 ATR kept it. StopMode SL_ATR_x3, |
//| TargetMode TP_NONE: the exit is the signal, not a price. |
//| |
//| REPLICATED ON THIS EA (2026-09-13, module alone, long only, 3-ATR |
//| stop, exit = signal, costs in), per trade in bp of notional: |
//| 2012-2026 SP500 +34.7 (n 84, 76% won) NAS100 +37.2 (145, 62%) |
//| US30 +22.8 (n 84, 68%) pooled ~+33 on 313, 3/3 |
//| 2019-2026 SP500 +46.2 (n 73, PF 2.32) NAS100 +41.4 (67, 1.56) |
//| US30 +25.0 (n 72, PF 1.37) - out of sample only |
//| The broker's SP500/US30 history is daily-only before 2017 (one |
//| tick per day, stamped in the shut hour), so those years cannot |
//| fill in the tester; the effective sample there starts 2018. |
//| |
//| CEILING, stated so it is not oversold: ~9 trades/yr/index, ~4%/yr |
//| on notional unlevered; the three-index basket at 1% risk is |
//| CAGR +2.6% at 10.6% max drawdown, and the losing years (2018, |
//| 2020, 2022) are the bear years on ALL THREE at once. Real, small, |
//| correlated. Size for the 5% account rule accordingly. |
//| |
//| THE SECOND FORM OF THE SAME DIP (2026-09-13, research/style_scan |
//| .py): seven strategy styles x 13 instruments x D1 and H4, honest |
//| protocol. Only MEAN REVERSION, LONG, on equity indices survived - |
//| and its deepest form is a z-score, not RSI(2): close more than z |
//| standard deviations below its 20-bar mean, exit at that mean or |
//| after 10 bars. On H4 SP500, 2019-2026, z<-1.5 made +22.7 bp per |
//| trade net (n 1,428, t 5.4) on Dukascopy and +18.5 (n 1,415, t 4.4)|
//| on the broker's own bars - the same variant on two data sources - |
//| at ~15 trades a month, which the RSI(2) daily form (8-14 a YEAR) |
//| never could. In-sample (<2019) it was positive but weak (+8.8), |
//| so it is an era-strengthened claim, stated as such. The z-score |
//| needs no trend filter in the scan (TrendPeriod 0 switches it off).|
//| WARRIOR_DIP_ENTRY lives in Enumerations\WarriorEnums.mqh. |
//| |
//| THE META-LABEL (System\DipMeta.mqh). With MetaCut above 0.50 the |
//| module asks an ALGLIB forest + MLP whether THIS dip pays before |
//| voting. The models are fitted here, in the terminal, on every |
//| past dip this same rule found and on what this same exit made of |
//| it - a simulation on closed bars, so the label is exactly the |
//| trade the EA would have taken. Refitted every MetaRefit bars from |
//| history-so-far: walk-forward, never saved. A skipped dip is still |
//| NAMED (Pattern_0) so the journal records its outcome too. |
//+------------------------------------------------------------------+
#ifndef WARRIOR_SIGNALDIPBUY_MQH
#define WARRIOR_SIGNALDIPBUY_MQH
#include "..\Expert\WarriorSignal.mqh"
#include "..\Enumerations\WarriorEnums.mqh"
#include "..\System\DipMeta.mqh"
class CSignalDipBuy : public CWarriorSignal
{
protected:
CiRSI m_rsi; // RSI(2) on close
int m_rsiPeriod;
double m_rsiEntry; // RSI(2) at or below this = a dip
int m_trendPeriod; // the long-term filter: close above this SMA (0 = off)
int m_exitPeriod; // exit when close crosses above this SMA
int m_maxBars; // time stop, in bars held
WARRIOR_DIP_ENTRY m_entry; // which dip
double m_zEntry; // z-score depth for DIP_ZSCORE
int m_zPeriod; // the mean and deviation window for the z-score
double m_stopAtr; // the stop the EA places, for the label simulation
int m_pattern_0; // the dip entry
int m_pattern_1; // the exit (voted as a "short" so ThresholdClose can act)
//--- THE META-LABEL
CDipMeta m_meta;
double m_metaCut; // <= 0.5 = off
int m_metaRefit; // refit every N bars
int m_metaMinBars; // history before the first attempt
int m_metaTrainedAtBars;
datetime m_metaLastTry;
int m_metaFits;
int m_metaAsked, m_metaSkipped;
double m_metaLastP;
double Sma(const int shift, const int period) const;
double ZScore(const int shift, const int period) const;
double Atr(const int shift, const int period) const;
int BarsHeld(void) const;
bool DipAt(const int shift) const;
bool BuildFeatures(double &x[], const int shift) const;
bool LabelAt(const int shift, double &label) const;
void MetaTrainIfDue(void);
bool MetaTrainFromHistory(void);
public:
CSignalDipBuy(void);
~CSignalDipBuy(void);
void RsiEntry(const double v) { m_rsiEntry = v; }
void ExitPeriod(const int v) { m_exitPeriod = v; }
void MaxBars(const int v) { m_maxBars = v; }
void TrendPeriod(const int v) { m_trendPeriod = v; }
void Entry(const WARRIOR_DIP_ENTRY e) { m_entry = e; }
void ZEntry(const double v) { m_zEntry = v; }
void StopAtr(const double v) { m_stopAtr = v; }
void MetaCut(const double v) { m_metaCut = v; }
void MetaRefit(const int v) { m_metaRefit = v; }
void Pattern_0(const int v) { m_pattern_0 = v; }
void Pattern_1(const int v) { m_pattern_1 = v; }
virtual void ApplyPatternWeight(int pattern, int weight)
{
if(pattern == 0) m_pattern_0 = weight;
if(pattern == 1) m_pattern_1 = weight;
}
virtual bool ValidationSettings(void) override;
virtual bool InitIndicators(CIndicators *indicators) override;
virtual int LongCondition(void) override;
virtual int ShortCondition(void) override;
};
//+------------------------------------------------------------------+
CSignalDipBuy::CSignalDipBuy(void) : m_rsiPeriod(2), m_rsiEntry(10.0), m_trendPeriod(200),
m_exitPeriod(5), m_maxBars(10),
m_entry(DIP_RSI2), m_zEntry(1.5), m_zPeriod(20), m_stopAtr(3.0),
m_pattern_0(100), m_pattern_1(100),
m_metaCut(0.0), m_metaRefit(500), m_metaMinBars(400),
m_metaTrainedAtBars(0), m_metaLastTry(0), m_metaFits(0),
m_metaAsked(0), m_metaSkipped(0), m_metaLastP(-1.0)
{
m_id = "DIPBUY";
m_pattern_count = 2;
m_used_series = USE_SERIES_OPEN + USE_SERIES_HIGH + USE_SERIES_LOW + USE_SERIES_CLOSE + USE_SERIES_TIME;
}
//+------------------------------------------------------------------+
CSignalDipBuy::~CSignalDipBuy(void)
{
if(m_metaCut > 0.5)
PrintFormat("CSignalDipBuy: META - %d fit(s); asked %d time(s), skipped %d (%.0f%%); last forest AUC %.3f, MLP AUC %.3f.",
m_metaFits, m_metaAsked, m_metaSkipped,
(m_metaAsked > 0 ? 100.0 * m_metaSkipped / m_metaAsked : 0.0),
m_meta.ForestAUC(), m_meta.MlpAUC());
}
//+------------------------------------------------------------------+
bool CSignalDipBuy::ValidationSettings(void)
{
if(!CWarriorSignal::ValidationSettings())
return false;
if(m_rsiEntry <= 0.0 || m_rsiEntry >= 50.0 || m_exitPeriod < 2 ||
(m_trendPeriod != 0 && m_trendPeriod < 50) || m_zEntry < 0.5 || m_zEntry > 4.0 || m_maxBars < 1)
{
Print("CSignalDipBuy: settings outside the measured neighbourhood (RSI 5-25, z 1.5-2.5, exit sma5..sma20, trend 0 or >=50)");
return false;
}
return true;
}
//+------------------------------------------------------------------+
bool CSignalDipBuy::InitIndicators(CIndicators *indicators)
{
if(indicators == NULL || !CWarriorSignal::InitIndicators(indicators))
return false;
if(!indicators.Add(GetPointer(m_rsi)) ||
!m_rsi.Create(m_symbol.Name(), m_period, m_rsiPeriod, PRICE_CLOSE))
{
Print("CSignalDipBuy: could not create RSI");
return false;
}
return true;
}
//+------------------------------------------------------------------+
double CSignalDipBuy::Sma(const int shift, const int period) const
{
double s = 0.0;
for(int i = 0; i < period; i++)
s += Close(shift + i);
return s / period;
}
//+------------------------------------------------------------------+
//| (close - mean) / standard deviation over `period` closed bars, |
//| the population deviation as the scan computed it. |
//+------------------------------------------------------------------+
double CSignalDipBuy::ZScore(const int shift, const int period) const
{
double s = 0.0, s2 = 0.0;
for(int i = 0; i < period; i++)
{
const double c = Close(shift + i);
if(c <= 0.0)
return 0.0; // past the series buffer: no dip, not a deep one
s += c; s2 += c * c;
}
const double mean = s / period;
const double var = s2 / period - mean * mean;
if(var <= 0.0)
return 0.0;
return (Close(shift) - mean) / MathSqrt(var);
}
//+------------------------------------------------------------------+
//| Average true range over `period` bars from `shift` - the same |
//| arithmetic as iATR, so the simulated stop is the one the vote |
//| places. |
//+------------------------------------------------------------------+
double CSignalDipBuy::Atr(const int shift, const int period) const
{
double s = 0.0;
for(int i = 0; i < period; i++)
{
const int b = shift + i;
const double pc = Close(b + 1);
double tr = High(b) - Low(b);
tr = MathMax(tr, MathAbs(High(b) - pc));
tr = MathMax(tr, MathAbs(Low(b) - pc));
s += tr;
}
return s / period;
}
//+------------------------------------------------------------------+
int CSignalDipBuy::BarsHeld(void) const
{
if(!PositionSelect(m_symbol.Name()))
return 0;
return iBarShift(m_symbol.Name(), m_period, (datetime)PositionGetInteger(POSITION_TIME));
}
//+------------------------------------------------------------------+
//| The dip rule at any closed bar, so history is judged by exactly |
//| the rule that fires live. RSI is read from the indicator buffer, |
//| which is deepened with the prices. |
//+------------------------------------------------------------------+
bool CSignalDipBuy::DipAt(const int shift) const
{
bool dip = false;
if(m_entry == DIP_ZSCORE)
dip = (ZScore(shift, m_zPeriod) <= -m_zEntry);
else
{
const double rsi = m_rsi.Main(shift);
if(rsi == EMPTY_VALUE || !MathIsValidNumber(rsi))
return false;
dip = (rsi <= m_rsiEntry);
}
//--- A trend average that reaches past the buffer averages in zeros and reads as "above".
const bool trendOk = (m_trendPeriod <= 0) ||
(Close(shift + m_trendPeriod) > 0.0 && Close(shift) > Sma(shift, m_trendPeriod));
return dip && trendOk;
}
//+------------------------------------------------------------------+
//| The state at the signal bar - nothing after it is read. |
//+------------------------------------------------------------------+
bool CSignalDipBuy::BuildFeatures(double &x[], const int shift) const
{
ArrayResize(x, DIPMETA_FEATURES);
const double atr = Atr(shift, 14);
if(atr <= 0.0 || !MathIsValidNumber(atr))
return false;
const double atr100 = Atr(shift, 100);
const double c = Close(shift), o = Open(shift), h = High(shift), l = Low(shift);
double hi20 = h, lo20 = l;
for(int i = 1; i < 20; i++)
{
hi20 = MathMax(hi20, High(shift + i));
lo20 = MathMin(lo20, Low(shift + i));
}
int streak = 0;
for(int i = 0; i < 10; i++)
{
if(Close(shift + i) < Close(shift + i + 1)) streak++;
else break;
}
MqlDateTime t;
TimeToStruct(Time(shift), t);
x[0] = ZScore(shift, m_zPeriod);
x[1] = (c - Sma(shift, 200)) / atr;
x[2] = (c - Close(shift + 1)) / atr;
x[3] = (c - Close(shift + 5)) / atr;
x[4] = streak;
x[5] = (hi20 > lo20) ? (c - lo20) / (hi20 - lo20) : 0.5;
x[6] = (h - l) / atr;
x[7] = (h > l) ? (c - l) / (h - l) : 0.5;
x[8] = (o - Close(shift + 1)) / atr;
x[9] = (atr100 > 0.0) ? atr / atr100 : 1.0;
x[10] = EfficiencyRatio(shift, 20);
x[11] = VarianceRatio(shift, 60, 5);
x[12] = RegimeCode(shift);
x[13] = t.day_of_week / 6.0;
x[14] = t.hour / 24.0;
for(int f = 0; f < DIPMETA_FEATURES; f++)
if(!MathIsValidNumber(x[f]))
return false;
return true;
}
//+------------------------------------------------------------------+
//| WHAT THIS MODULE'S OWN EXIT MADE OF THE DIP AT `shift`: fill at |
//| the next bar's open; a stop m_stopAtr ATR below it, filled at the |
//| stop; else the first close above the exit average, filled at the |
//| open after it (the vote reads the closed bar and acts on the next |
//| tick); else the time stop, filled the same way. 1 = it paid. |
//+------------------------------------------------------------------+
bool CSignalDipBuy::LabelAt(const int shift, double &label) const
{
if(shift < m_maxBars + 2)
return false; // the outcome is not fully on closed bars yet
const double atr = Atr(shift, 14);
if(atr <= 0.0)
return false;
const double fill = Open(shift - 1);
const double stop = fill - m_stopAtr * atr;
double exitPx = 0.0;
for(int k = 1; k <= m_maxBars; k++)
{
const int b = shift - k;
if(m_stopAtr > 0.0 && Low(b) <= stop)
{ exitPx = stop; break; }
if(Close(b) > Sma(b, m_exitPeriod) || k == m_maxBars)
{ exitPx = Open(b - 1); break; }
}
if(exitPx <= 0.0)
return false;
label = (exitPx > fill) ? 1.0 : 0.0;
return true;
}
//+------------------------------------------------------------------+
//| Rebuild the rows from history-so-far and refit. Bars() in the |
//| tester is history accrued to the bar being decided, so the fit |
//| can only ever have seen the past. |
//+------------------------------------------------------------------+
bool CSignalDipBuy::MetaTrainFromHistory(void)
{
const int bars = Bars(m_symbol.Name(), m_period);
const int first = 310; // ATR100 + SMA200 + the label's own window, with margin
const int last = bars - 2;
if(last - first < 100)
return false;
const int want = MathMin(bars, WARRIOR_NET_HISTORY);
const bool deepRsi = (want <= m_rsi.BufferSize()) || m_rsi.BufferResize(want);
if(!DeepenPrices(want) || !deepRsi)
{
PrintFormat("CSignalDipBuy: could not deepen series to %d bar(s) - meta not trained.", want);
return false;
}
CMatrixDouble xy(last - first + 1, DIPMETA_FEATURES + 1);
int rows = 0, dips = 0;
double x[], label;
for(int s = last; s >= first; s--) // oldest to newest: the split is chronological
{
if(!DipAt(s))
continue;
dips++;
if(!BuildFeatures(x, s) || !LabelAt(s, label))
continue;
for(int f = 0; f < DIPMETA_FEATURES; f++)
xy.Set(rows, f, x[f]);
xy.Set(rows, DIPMETA_FEATURES, label);
rows++;
}
PrintFormat("CSignalDipBuy: META fit #%d at %d bars - %d dip(s) in history, %d labelled row(s).",
m_metaFits + 1, bars, dips, rows);
//--- Embargo = the label's window: the last training dips share their outcome bars with the
//--- first validation dips. Dips cluster, so two horizons rather than one.
return m_meta.Train(xy, rows, 2 * (m_maxBars + 2));
}
//+------------------------------------------------------------------+
void CSignalDipBuy::MetaTrainIfDue(void)
{
if(m_metaCut <= 0.5)
return;
const datetime bar = iTime(m_symbol.Name(), m_period, 0);
if(bar == m_metaLastTry)
return;
const int bars = Bars(m_symbol.Name(), m_period);
if(bars < m_metaMinBars)
return;
if(m_metaTrainedAtBars > 0 && (m_metaRefit <= 0 || bars < m_metaTrainedAtBars + m_metaRefit))
return;
m_metaLastTry = bar;
m_metaTrainedAtBars = bars; // back off whether it succeeds or fails
if(MetaTrainFromHistory())
m_metaFits++;
}
//+------------------------------------------------------------------+
//| THE DIP: evaluated on the closed bar, acted on at the next open - |
//| exactly the "fill at next open" the screen priced. With the meta |
//| on, a dip the model rates below the cut is named but not voted. |
//+------------------------------------------------------------------+
int CSignalDipBuy::LongCondition(void)
{
MetaTrainIfDue();
const int idx = StartIndex();
if(!DipAt(idx))
return 0;
m_active_pattern = "Pattern_0";
m_active_direction = "Buy";
if(m_metaCut > 0.5 && m_meta.Ready())
{
double x[];
if(BuildFeatures(x, idx))
{
m_metaLastP = m_meta.Score(x);
if(m_metaLastP >= 0.0)
{
m_metaAsked++;
if(m_metaLastP < m_metaCut)
{
m_metaSkipped++;
return 0;
}
}
}
}
return m_pattern_0;
}
//+------------------------------------------------------------------+
//| THE EXIT, expressed as a short vote so the standard library's |
//| CheckCloseLong() (-direction >= ThresholdClose) can act on it. |
//| With Direction = DIR_LONG it can never OPEN a short. Two ways |
//| out: the close crosses back above the short average - the dip |
//| has reverted - or the time stop, because a dip that has not |
//| reverted in ten bars is not the setup that was measured. |
//+------------------------------------------------------------------+
int CSignalDipBuy::ShortCondition(void)
{
const int idx = StartIndex();
const bool reverted = (Close(idx) > Sma(idx, m_exitPeriod));
const bool timedOut = (m_maxBars > 0 && BarsHeld() >= m_maxBars);
if(reverted || timedOut)
{
m_active_pattern = "Pattern_1";
m_active_direction = "Sell";
return m_pattern_1;
}
return 0;
}
#endif // WARRIOR_SIGNALDIPBUY_MQH