Warrior_EA/Signals/SignalDipBuy.mqh
AnimateDread 75d7362161 feat(warrior): the vol-gated dip-buy book, ported into Warrior_EA
Warrior's defaults are now the validated book: DIP_ZSCORE alone, long only, H4,
risk 0.25%, one chart per index with a shared Magic.

- System/BarCache.mqh: whole-history closed bars, Wilder ATR, GK sigma and the
  expanding vol percentile (no 1024-bar stdlib ceiling)
- System/AccountGuard.mqh: open-risk cap, kill switch, cross-chart lock and
  Friday flat, shared through terminal globals by Magic
- CWarriorExpert: guard on every tick; a transient open failure retries the bar
- CWarriorSignal::SetupStop: the dip owns its 3 x Wilder ATR stop from the bid
- SignalDipBuy: no entry vote while holding (a still-dipping time exit never
  closed, and Processing re-entered on the exit bar); no entry on a stop bar
- WarriorMoney sizes on equity; WARRIOR_RISK allows fractional risk
- TradeLog + research/compare_ea.py: trade-for-trade check vs WarriorDipZ -
  SP500/US30/DAX40 identical to the cent, NAS100 96.9% (stale-quote timer fills)
- research/nn_cross_index.py: pre-registered cross-index NN meta-label - FAIL
  (AUC 0.564, CI [0.498, 0.630]); DipMetaCut stays off

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 21:14:53 -04:00

635 lines
29 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. |
//| |
//| THE VOL-GATED Z FORM - THE PRODUCTION RULE (2026-09-23). Ported |
//| rule for rule from mql5/WarriorDipZ.mq5, the reference EA that |
//| reconciled with research/backtest.py at 0.998 per trade: |
//| entry z20 <= -DipZ on the closed bar, filled at the next open |
//| gate Garman-Klass sigma(30) of that bar, as a causal |
//| EXPANDING-window percentile, >= DipVolPct (H4 only - on |
//| D1 it halves the cadence) |
//| exit first closed bar with close >= SMA20, else after 10 bars |
//| stop bid at the fill - 3 x WILDER ATR(14) of the signal bar |
//| The z form reads System\BarCache.mqh, not the stdlib series: the |
//| gate ranks against the whole history, past the 1024-bar ceiling. |
//| Two behaviours the stdlib path would get wrong, handled here: |
//| - while a position is held the ENTRY vote is 0. Otherwise a |
//| time exit on a bar that is still a dip nets long 100 against |
//| exit 100 to a vote of 0 and the position is never closed, and |
//| CExpert::Processing re-enters on the bar it just exited. |
//| - no entry on a signal bar during which a stop was hit - the |
//| research resumes scanning at exit bar + 1. |
//+------------------------------------------------------------------+
#ifndef WARRIOR_SIGNALDIPBUY_MQH
#define WARRIOR_SIGNALDIPBUY_MQH
#include "..\Expert\WarriorSignal.mqh"
#include "..\Enumerations\WarriorEnums.mqh"
#include "..\System\DipMeta.mqh"
#include "..\System\BarCache.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;
//--- THE VOL-GATED Z FORM (see the header)
CBarCache m_cache;
int m_atrPeriod; // Wilder ATR period for the stop
bool m_volGate;
double m_volPct; // gate: minimum expanding percentile of GK sigma
int m_volWindow; // GK sigma window
int m_volWarm; // bars before the gate can pass
double m_stopDist; // the stop distance this bar's dip asked for
datetime m_stopBar; // ...and the bar it was computed on
int m_gated, m_zDips;// dips seen / refused by the gate, reported at deinit
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 Holding(datetime &opened) const;
bool StoppedSince(const datetime barTime) const;
int ZLong(void);
int ZExit(void);
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 AtrPeriod(const int v) { m_atrPeriod = v; }
void VolGate(const bool on, const double pct) { m_volGate = on; m_volPct = pct; }
virtual bool SetupStop(const bool isLong, double &sl) override;
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_atrPeriod(14), m_volGate(false), m_volPct(0.5),
m_volWindow(30), m_volWarm(250), m_stopDist(0.0), m_stopBar(0),
m_gated(0), m_zDips(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_entry == DIP_ZSCORE)
PrintFormat("CSignalDipBuy: z form - %d dip(s) on closed bars while flat, %d refused by the"
" vol gate (%s), %d bar(s) in the cache.", m_zDips, m_gated,
(m_volGate ? "pct >= " + DoubleToString(m_volPct, 2) : "off"), m_cache.Count());
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;
m_cache.Init(m_symbol.Name(), m_period, m_atrPeriod, m_volWindow);
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
{
datetime opened = 0;
if(!Holding(opened))
return 0;
return iBarShift(m_symbol.Name(), m_period, opened);
}
//+------------------------------------------------------------------+
//| This chart's position - symbol AND magic, so a hedging account |
//| or another EA on the same symbol is never mistaken for ours. |
//+------------------------------------------------------------------+
bool CSignalDipBuy::Holding(datetime &opened) const
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
const ulong tk = PositionGetTicket(i);
if(tk == 0)
continue;
if(PositionGetString(POSITION_SYMBOL) == m_symbol.Name() &&
(ulong)PositionGetInteger(POSITION_MAGIC) == m_magic)
{
opened = (datetime)PositionGetInteger(POSITION_TIME);
return true;
}
}
return false;
}
//+------------------------------------------------------------------+
//| Was one of our positions on this symbol stopped out during or |
//| after the bar that opened at barTime? |
//+------------------------------------------------------------------+
bool CSignalDipBuy::StoppedSince(const datetime barTime) const
{
if(!HistorySelect(barTime, TimeCurrent() + 60))
return false;
for(int i = HistoryDealsTotal() - 1; i >= 0; i--)
{
const ulong d = HistoryDealGetTicket(i);
if(d == 0 || HistoryDealGetString(d, DEAL_SYMBOL) != m_symbol.Name() ||
(ulong)HistoryDealGetInteger(d, DEAL_MAGIC) != m_magic ||
HistoryDealGetInteger(d, DEAL_ENTRY) != DEAL_ENTRY_OUT)
continue;
const long why = HistoryDealGetInteger(d, DEAL_REASON);
if(why == DEAL_REASON_SL || why == DEAL_REASON_SO)
return true;
}
return false;
}
//+------------------------------------------------------------------+
//| THE Z FORM'S ENTRY, on the cache's newest closed bar. |
//+------------------------------------------------------------------+
int CSignalDipBuy::ZLong(void)
{
datetime opened = 0;
if(Holding(opened) || !m_cache.Sync())
return 0;
const int i = m_cache.Last();
if(m_cache.Atr(i) <= 0.0)
return 0;
double mean = 0.0, sd = 0.0;
if(!m_cache.MeanStd(i, m_zPeriod, mean, sd) || sd <= 0.0 ||
(m_cache.Close(i) - mean) / sd > -m_zEntry)
return 0;
if(StoppedSince(m_cache.Time(i)))
return 0;
m_zDips++;
m_active_pattern = "Pattern_0";
m_active_direction = "Buy";
if(m_volGate && m_cache.VolPercentile(i, m_volWarm) < m_volPct)
{
m_gated++;
return 0;
}
m_stopDist = m_stopAtr * m_cache.Atr(i);
m_stopBar = iTime(m_symbol.Name(), m_period, 0);
return m_pattern_0;
}
//+------------------------------------------------------------------+
//| THE Z FORM'S EXIT: close back at or above the mean, or time. |
//+------------------------------------------------------------------+
int CSignalDipBuy::ZExit(void)
{
datetime opened = 0;
if(!Holding(opened) || !m_cache.Sync())
return 0;
const int i = m_cache.Last();
double mean = 0.0, sd = 0.0;
const bool reverted = m_cache.MeanStd(i, m_exitPeriod, mean, sd) && m_cache.Close(i) >= mean;
//--- bars held = shift of the bar containing the fill; the fill bar counts, so the time exit
//--- lands on the close of bar fill+9 - exactly the reference EA's count.
const bool timedOut = (m_maxBars > 0 &&
iBarShift(m_symbol.Name(), m_period, opened, false) >= m_maxBars);
if(!reverted && !timedOut)
return 0;
m_active_pattern = "Pattern_1";
m_active_direction = "Sell";
return m_pattern_1;
}
//+------------------------------------------------------------------+
//| The stop the research priced: the BID at the fill (the backtest's |
//| fill is a bid open, and a long's stop triggers on the bid) minus |
//| StopAtr x Wilder ATR of the signal bar, floored to the tick. |
//| Answered only on the bar whose dip asked for it. |
//+------------------------------------------------------------------+
bool CSignalDipBuy::SetupStop(const bool isLong, double &sl)
{
if(!isLong || m_entry != DIP_ZSCORE || m_stopDist <= 0.0 ||
m_stopBar != iTime(m_symbol.Name(), m_period, 0))
return false;
double stop = m_symbol.Bid() - m_stopDist;
const double tsz = m_symbol.TickSize();
if(tsz > 0.0)
stop = MathFloor(stop / tsz) * tsz;
sl = stop;
return true;
}
//+------------------------------------------------------------------+
//| 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(m_entry == DIP_ZSCORE)
{
const int vote = ZLong();
if(vote <= 0 || m_metaCut <= 0.5 || !m_meta.Ready())
return vote;
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 vote;
}
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)
{
if(m_entry == DIP_ZSCORE)
return ZExit();
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