- 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.
532 lines
27 KiB
MQL5
532 lines
27 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| SignalNeural.mqh |
|
|
//| AnimateDread |
|
|
//| |
|
|
//| A NEURAL NETWORK AS AN ORDINARY SIGNAL MODULE. |
|
|
//| |
|
|
//| It derives from CWarriorSignal, implements LongCondition() and |
|
|
//| ShortCondition() returning 0..100, and that is the whole of its |
|
|
//| contract with the rest of the EA. The vote cannot tell it apart |
|
|
//| from CSignalMA, and nothing in the parent knows a network exists. |
|
|
//| |
|
|
//| That is deliberate, and it is a return to how this repo started. |
|
|
//| At 0a527b0 an NN module did exactly this. At 2f5adb4 it gained |
|
|
//| SignedAIConfidence(), and from then on the parent had to special- |
|
|
//| case it: confidence-scaled stops, confidence-scaled lots, tier |
|
|
//| ladders, LiveVote(), VoteCapableWeight(), IsAIFilter(). None of |
|
|
//| that made money and all of it made the net unrankable, because a |
|
|
//| thing that votes in its own private currency cannot be compared |
|
|
//| with the modules it votes alongside. |
|
|
//| |
|
|
//| IT BUILDS ITS OWN INPUTS. There is no separate feature-builder |
|
|
//| file: a module that votes owns what it votes on. The names below |
|
|
//| are the contract the model file is checked against, so a model |
|
|
//| trained on a different column set is refused rather than fed. |
|
|
//| |
|
|
//| WHAT IT PREDICTS. Not direction - this project measured direction |
|
|
//| dead at bar level (rho 0.00-0.02). It predicts whether the bar's |
|
|
//| low survives the next two bars: the unknown half of a Bill |
|
|
//| Williams fractal. Measured 2026-09-11 on 20 instrument/timeframe |
|
|
//| series, out of sample, chronological split: AUC 0.772 (range |
|
|
//| 0.756-0.792). Mean R by predicted-probability quintile ran -0.230 |
|
|
//| / -0.089 / -0.026 / +0.002 / +0.017, monotone on 20 of 20. |
|
|
//| |
|
|
//| ⚠ SO ITS VALUE IS A REFUSAL, NOT A SELECTION. |
|
|
//| Its value is in refusing the worst quintile, not in selecting the |
|
|
//| best: the top quintile is breakeven before costs. A module that |
|
|
//| returned a big number on a high score would be claiming an edge |
|
|
//| the measurement does not support. |
|
|
//| |
|
|
//| The standard library has no way to refuse ONE side, and the note |
|
|
//| above ShortCondition() works through what it does offer and what |
|
|
//| this module therefore assumes. When a comment and the code |
|
|
//| disagree, believe the journal: it counts what the module said. |
|
|
//+------------------------------------------------------------------+
|
|
#ifndef WARRIOR_SIGNALNEURAL_MQH
|
|
#define WARRIOR_SIGNALNEURAL_MQH
|
|
|
|
#include "..\Expert\WarriorSignal.mqh"
|
|
#include "..\System\WarriorNet.mqh"
|
|
#include "Wyckoff\WyckoffFeed.mqh"
|
|
#include "..\System\AltDataFeed.mqh"
|
|
|
|
//--- 9 price shape + 9 Wyckoff + 3 volume + 4 calendar + 16 alt data.
|
|
#define NEURAL_PRICE_F 9
|
|
#define NEURAL_WYK_F 9
|
|
#define NEURAL_VOL_F 3
|
|
#define NEURAL_TIME_F 4
|
|
#define NEURAL_FEATURES (NEURAL_PRICE_F + NEURAL_WYK_F + NEURAL_VOL_F + NEURAL_TIME_F + ALT_COLUMNS)
|
|
|
|
class CSignalNeural : public CWarriorSignal
|
|
{
|
|
protected:
|
|
CWarriorNet m_net;
|
|
CiATR m_atr;
|
|
int m_atrPeriod;
|
|
int m_pattern_0; // the refusal, voted as a SHORT - see ShortCondition()
|
|
int m_pattern_1; // the mild confirmation a high score earns
|
|
double m_cut; // p <= 1-cut is the refusal band, p >= cut the confirmation
|
|
bool m_trained;
|
|
//--- THE SCORE, COMPUTED ONCE PER BAR. LongCondition() and ShortCondition() are both called on
|
|
//--- the same evaluation by Direction() (ExpertSignal.mqh:431) and must read the SAME number -
|
|
//--- scoring twice could answer two different things if anything underneath refreshed between
|
|
//--- the calls, and the two bands would then no longer be mutually exclusive.
|
|
datetime m_scoreBar;
|
|
double m_score; // <0 when this bar has no usable score
|
|
double Score(void);
|
|
//--- Monotone, bounded, stateless: maps any real into (-1,1) while preserving order. Used where
|
|
//--- a feature has no natural scale and a stored mean/variance would be one more thing to keep
|
|
//--- in step with the model file.
|
|
static double Squash(const double v) { return v / (1.0 + MathAbs(v)); }
|
|
//--- DEFERRED TRAINING. In the tester Bars() at OnInit returns almost nothing - history accrues
|
|
//--- AS THE RUN PROGRESSES - so training at init can never work in a backtest. Training partway
|
|
//--- through instead is not a workaround, it is the correct shape: Bars() then returns
|
|
//--- history-so-far, so the model can only ever have been fitted on the past and every bar it
|
|
//--- votes on is genuinely out of sample. Retraining on an interval makes it walk-forward.
|
|
int m_minBars; // do not train until this much history exists
|
|
int m_retrainBars; // retrain every N bars (0 = train once)
|
|
datetime m_lastTrainBar;
|
|
int m_trainCount;
|
|
int m_trainedAtBars;
|
|
void TrainIfDue(void);
|
|
|
|
//--- The column contract. Order is the contract; append only, never insert.
|
|
static string FeatureName(const int i);
|
|
bool BuildFeatures(double &x[], const int shift);
|
|
//--- P(this bar's low survives the next two) - the fractal's unknown half.
|
|
bool LabelAt(const int shift, double &label);
|
|
bool TrainFromHistory(void);
|
|
string ModelPath(void) const
|
|
{ return "Warrior_EA\\Nets\\" + m_symbol.Name() + "_" +
|
|
IntegerToString(m_period) + "_fractal.net"; }
|
|
|
|
public:
|
|
CSignalNeural(void);
|
|
~CSignalNeural(void) {}
|
|
void AtrPeriod(const int v) { m_atrPeriod = v; }
|
|
void MinBars(const int v) { m_minBars = v; }
|
|
void RetrainBars(const int v) { m_retrainBars = v; }
|
|
void Confidence(const double v) { m_cut = 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 InitIndicators(CIndicators *indicators) override;
|
|
virtual int LongCondition(void) override;
|
|
virtual int ShortCondition(void) override;
|
|
};
|
|
//+------------------------------------------------------------------+
|
|
CSignalNeural::CSignalNeural(void) : m_atrPeriod(14), m_pattern_0(40), m_pattern_1(15),
|
|
m_cut(0.50), m_trained(false),
|
|
m_minBars(750), m_retrainBars(500),
|
|
m_lastTrainBar(0), m_trainCount(0), m_trainedAtBars(0),
|
|
m_scoreBar(0), m_score(-1.0)
|
|
{
|
|
m_id = "NEURAL";
|
|
m_pattern_count = 2;
|
|
m_used_series = USE_SERIES_OPEN + USE_SERIES_HIGH + USE_SERIES_LOW + USE_SERIES_CLOSE +
|
|
USE_SERIES_TICK_VOLUME;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
string CSignalNeural::FeatureName(const int i)
|
|
{
|
|
switch(i)
|
|
{
|
|
//--- Price shape, all scale-free. A raw price here would teach the net the era, not the market.
|
|
case 0: return "close_in_bar"; // where the close sat in its own range
|
|
case 1: return "range_atr"; // this bar's range, in ATR
|
|
case 2: return "below_prior_low_atr"; // how far the low undercut the previous one
|
|
case 3: return "ret1_atr"; // last return
|
|
case 4: return "ret3_atr";
|
|
case 5: return "dist_sma50_atr"; // trend context
|
|
case 6: return "prior_range_atr";
|
|
case 7: return "upper_wick_atr";
|
|
case 8: return "lower_wick_atr";
|
|
//--- Wyckoff structure, read through the shared feed.
|
|
case 9: return "wyk_event_signed";
|
|
case 10: return "wyk_phase_signed";
|
|
case 11: return "wyk_spring_grade";
|
|
case 12: return "wyk_character";
|
|
case 13: return "wyk_range_open";
|
|
case 14: return "wyk_range_pos";
|
|
case 15: return "wyk_dist_creek_atr";
|
|
case 16: return "wyk_fail_value";
|
|
case 17: return "wyk_bar_quality";
|
|
//--- VOLUME, and never raw. Tick volume trends upward over a decade as the feed densifies,
|
|
//--- so a raw count teaches the net which YEAR it is looking at - the same trap as a raw
|
|
//--- price. All three are ratios against the recent past, which is scale-free and era-free.
|
|
case 18: return "vol_rel_sma20";
|
|
case 19: return "vol_rel_sma5";
|
|
case 20: return "vol_chg1";
|
|
//--- TIME, CYCLICALLY ENCODED. The previous generation of nets fed CLOCK time as an ordinal,
|
|
//--- which asserts that Friday is five times Monday and that December is twelve times
|
|
//--- January, and puts a discontinuity between the last bar of one week and the first of the
|
|
//--- next. sin/cos pairs make the wrap continuous and the ordering honest. On D1 there is no
|
|
//--- intraday session to encode - the useful periodicities are the week and the year.
|
|
case 21: return "dow_sin";
|
|
case 22: return "dow_cos";
|
|
case 23: return "month_sin";
|
|
case 24: return "month_cos";
|
|
}
|
|
//--- THE ALT BLOCK, appended last so the existing columns keep their indices - the model file is
|
|
//--- validated against these names, so an insert would invalidate every net ever trained.
|
|
if(i >= 25 && i < 25 + ALT_COLUMNS)
|
|
return "alt_" + CAltDataFeed::ColumnName(i - 25);
|
|
return "?";
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
bool CSignalNeural::BuildFeatures(double &x[], const int shift)
|
|
{
|
|
ArrayResize(x, NEURAL_FEATURES);
|
|
ArrayInitialize(x, 0.0);
|
|
const double atr = m_atr.Main(shift);
|
|
if(atr <= 0.0 || !MathIsValidNumber(atr))
|
|
return false;
|
|
const double o = Open(shift), h = High(shift), l = Low(shift), c = Close(shift);
|
|
const double rng = h - l;
|
|
if(rng <= 0.0)
|
|
return false;
|
|
double sma = 0.0;
|
|
for(int k = 0; k < 50; k++)
|
|
sma += Close(shift + k);
|
|
sma /= 50.0;
|
|
|
|
int i = 0;
|
|
x[i++] = (c - l) / rng;
|
|
x[i++] = rng / atr;
|
|
x[i++] = (Low(shift + 1) - l) / atr;
|
|
x[i++] = (c - Close(shift + 1)) / atr;
|
|
x[i++] = (c - Close(shift + 3)) / atr;
|
|
x[i++] = (c - sma) / atr;
|
|
x[i++] = (High(shift + 1) - Low(shift + 1)) / atr;
|
|
x[i++] = (h - MathMax(o, c)) / atr;
|
|
x[i++] = (MathMin(o, c) - l) / atr;
|
|
|
|
//--- The Wyckoff half. The feed zeroes what it cannot say, and zero is a legitimate "no
|
|
//--- structure here" for every one of these - so no have/haven't flag is needed.
|
|
x[i++] = g_wyckoffFeed.Event(shift) / 9.0;
|
|
x[i++] = g_wyckoffFeed.Phase(shift) / 5.0;
|
|
x[i++] = g_wyckoffFeed.SpringGrade(shift) / 3.0;
|
|
x[i++] = g_wyckoffFeed.Character(shift);
|
|
const bool open = g_wyckoffFeed.RangeOpen(shift);
|
|
x[i++] = open ? 1.0 : 0.0;
|
|
const double top = g_wyckoffFeed.ZoneTop(shift), bot = g_wyckoffFeed.ZoneBottom(shift);
|
|
//--- 0.5 when there is no range: a ratio against a zero-width range is not a small number, it is
|
|
//--- a wrong one, and the midpoint is the honest "no information" value for a position feature.
|
|
x[i++] = (open && (top - bot) > 0.0) ? ((c - bot) / (top - bot)) : 0.5;
|
|
const double creek = g_wyckoffFeed.Creek(shift);
|
|
x[i++] = (creek != 0.0) ? ((c - creek) / atr) : 0.0;
|
|
x[i++] = g_wyckoffFeed.FailValue(shift) / 2.5;
|
|
x[i++] = g_wyckoffFeed.BarQuality(shift) / 2.0;
|
|
|
|
//--- VOLUME. A bar that moved on twice its usual participation is a different bar from one that
|
|
//--- drifted on nothing, and the price-shape columns above cannot express that at all.
|
|
double v20 = 0.0, v5 = 0.0;
|
|
for(int k = 0; k < 20; k++)
|
|
{
|
|
const double v = (double)TickVolume(shift + k);
|
|
v20 += v;
|
|
if(k < 5)
|
|
v5 += v;
|
|
}
|
|
v20 /= 20.0;
|
|
v5 /= 5.0;
|
|
const double vNow = (double)TickVolume(shift);
|
|
const double vPrev = (double)TickVolume(shift + 1);
|
|
//--- A dead bar would divide by zero; refuse the row rather than emit an infinity that
|
|
//--- MathIsValidNumber would pass and the trainer would choke on.
|
|
if(v20 <= 0.0 || v5 <= 0.0 || vPrev <= 0.0)
|
|
return false;
|
|
x[i++] = Squash(vNow / v20 - 1.0);
|
|
x[i++] = Squash(vNow / v5 - 1.0);
|
|
x[i++] = Squash(vNow / vPrev - 1.0);
|
|
|
|
//--- CALENDAR POSITION, cyclic. See FeatureName() for why this is not an ordinal.
|
|
MqlDateTime t;
|
|
TimeToStruct(iTime(m_symbol.Name(), m_period, shift), t);
|
|
const double dow = 2.0 * M_PI * t.day_of_week / 7.0;
|
|
const double mon = 2.0 * M_PI * (t.mon - 1) / 12.0;
|
|
x[i++] = MathSin(dow);
|
|
x[i++] = MathCos(dow);
|
|
x[i++] = MathSin(mon);
|
|
x[i++] = MathCos(mon);
|
|
|
|
//--- THE ALT BLOCK. Absent (pre-2010) means the ROW IS DROPPED, never zero-filled - see the
|
|
//--- header of AltDataFeed.mqh. Squashed rather than z-scored: v/(1+|v|) is monotone, bounded,
|
|
//--- and stateless, so it needs no training-set statistics to be stored in the model file and
|
|
//--- cannot leak one fold's scale into another.
|
|
double alt[];
|
|
if(!g_altData.Lookup(iTime(m_symbol.Name(), m_period, shift), alt))
|
|
return false;
|
|
for(int c2 = 0; c2 < ALT_COLUMNS; c2++)
|
|
x[i++] = Squash(alt[c2]);
|
|
|
|
return (i == NEURAL_FEATURES);
|
|
}
|
|
//| THE LABEL. Known only at shift-2, so training must never use a |
|
|
//| bar newer than that - which is what the offset in the loop below |
|
|
//| enforces. Predicting it at `shift` is a genuine two-bar-ahead |
|
|
//| question, not a restatement of the present. |
|
|
//+------------------------------------------------------------------+
|
|
bool CSignalNeural::LabelAt(const int shift, double &label)
|
|
{
|
|
if(shift < 2)
|
|
return false;
|
|
const double l = Low(shift);
|
|
label = (l < Low(shift - 1) && l < Low(shift - 2)) ? 1.0 : 0.0;
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
bool CSignalNeural::TrainFromHistory(void)
|
|
{
|
|
const int bars = Bars(m_symbol.Name(), m_period);
|
|
//--- Leave the newest bars alone: the label needs two bars after it, and the features need the
|
|
//--- 50 before it. Both ends are trimmed rather than clamped, so no row is built from a window
|
|
//--- that does not exist.
|
|
const int first = 60, last = bars - 3;
|
|
if(last - first < 200)
|
|
{
|
|
Print("CSignalNeural: not enough history to train.");
|
|
return false;
|
|
}
|
|
//--- REACH PAST THE STANDARD LIBRARY'S 1024-BAR BUFFER BEFORE WALKING HISTORY.
|
|
//---
|
|
//--- Without this the sweep below reads 0.0 for every shift past 1023 and the rows simply stop
|
|
//--- appearing - which is how this module trained on exactly 915 rows of an 11-year run, four
|
|
//--- times, without ever printing a warning. Ask for what the run has actually accrued, never
|
|
//--- more: in the tester Bars() is history-so-far, so this cannot reach past the bar being
|
|
//--- decided and the fit stays walk-forward.
|
|
//---
|
|
//--- THE TWO MUST GROW TOGETHER OR NOT AT ALL. The Wyckoff half of the feature vector reports
|
|
//--- 0.0 for "no structure here", and a buffer that has run out reports the same 0.0. So deep
|
|
//--- prices with a shallow feed would not lose rows - it would manufacture ~2,000 rows whose
|
|
//--- nine structure features are all zero, and teach the net that old bars are featureless.
|
|
//--- That is strictly worse than the shortfall it replaces, so a half-failure refuses.
|
|
//--- The ATR is in the same group and for the same reason: BuildFeatures() divides nine of its
|
|
//--- eighteen columns by it and refuses the row when it is not positive, so an ATR left at 1024
|
|
//--- would re-impose the exact ceiling the other two just lifted.
|
|
const int want = MathMin(bars, WARRIOR_NET_HISTORY);
|
|
const bool deepAtr = (want <= m_atr.BufferSize()) || m_atr.BufferResize(want);
|
|
if(!DeepenPrices(want) || !deepAtr || !g_wyckoffFeed.Deepen(want))
|
|
{
|
|
PrintFormat("CSignalNeural: could not deepen series to %d bar(s) - not trained rather than "
|
|
"trained on a truncated or zero-filled window.", want);
|
|
return false;
|
|
}
|
|
CMatrixDouble xy(last - first, NEURAL_FEATURES + 1);
|
|
int rows = 0;
|
|
double x[], label;
|
|
for(int s = last; s >= first; s--) // oldest to newest: the split below is chronological
|
|
{
|
|
if(!BuildFeatures(x, s) || !LabelAt(s, label))
|
|
continue;
|
|
bool ok = true;
|
|
for(int f = 0; f < NEURAL_FEATURES; f++)
|
|
if(!MathIsValidNumber(x[f]))
|
|
{ ok = false; break; }
|
|
if(!ok)
|
|
continue;
|
|
for(int f = 0; f < NEURAL_FEATURES; f++)
|
|
xy.Set(rows, f, x[f]);
|
|
xy.Set(rows, NEURAL_FEATURES, label);
|
|
rows++;
|
|
}
|
|
if(rows < 100)
|
|
{
|
|
Print("CSignalNeural: only ", rows, " usable row(s) - not trained.");
|
|
return false;
|
|
}
|
|
//--- SAY WHAT WAS DROPPED. A sweep that silently yields a third of its candidates is exactly the
|
|
//--- failure this module already shipped once, and it was invisible because the only number ever
|
|
//--- printed was the one that survived. A row count means nothing without its denominator.
|
|
const int candidates = last - first;
|
|
if(rows < candidates)
|
|
PrintFormat("CSignalNeural: %d of %d candidate bar(s) usable (%.0f%%) - %d dropped for an "
|
|
"invalid ATR, a zero range or a non-finite feature.",
|
|
rows, candidates, 100.0 * rows / candidates, candidates - rows);
|
|
string names[];
|
|
ArrayResize(names, NEURAL_FEATURES);
|
|
for(int f = 0; f < NEURAL_FEATURES; f++)
|
|
names[f] = FeatureName(f);
|
|
//--- Embargo of two rows: the label needs the two bars AFTER its own, so the last two training
|
|
//--- rows share their answer with the first validation rows. Small here - a 2-bar window against
|
|
//--- the management net's 60 - but the same leak, and a 0.5% cost to be sure of the tail.
|
|
if(!m_net.Train(xy, rows, NEURAL_FEATURES, names, 2))
|
|
{
|
|
Print("CSignalNeural: training refused - ", m_net.Why());
|
|
return false;
|
|
}
|
|
m_net.Save(ModelPath(), names,
|
|
StringFormat("fractal-low survival, %s %s, %d rows", m_symbol.Name(),
|
|
EnumToString((ENUM_TIMEFRAMES)m_period), rows));
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
bool CSignalNeural::InitIndicators(CIndicators *indicators)
|
|
{
|
|
if(indicators == NULL || !CWarriorSignal::InitIndicators(indicators))
|
|
return false;
|
|
if(!indicators.Add(GetPointer(m_atr)) || !m_atr.Create(m_symbol.Name(), m_period, m_atrPeriod))
|
|
{
|
|
Print("CSignalNeural: could not create ATR");
|
|
return false;
|
|
}
|
|
if(!WyckoffFeedEnsure(indicators, m_symbol.Name(), m_period))
|
|
{
|
|
Print("CSignalNeural: Wyckoff feed unavailable - ", g_wyckoffFeed.Why());
|
|
return false;
|
|
}
|
|
//--- THE ALT BLOCK IS MANDATORY, not best-effort. Sixteen of the forty-one inputs come from it,
|
|
//--- and a net silently trained on sixteen zero columns is worse than no net: it would look
|
|
//--- trained, pass every check, and have spent a third of its width on nothing.
|
|
if(!g_altData.Load(m_symbol.Name()))
|
|
{
|
|
Print("CSignalNeural: alt data unavailable - ", g_altData.Why(),
|
|
" - needs Common/Files/ADAltData/<symbol>_D1.csv");
|
|
return false;
|
|
}
|
|
PrintFormat("CSignalNeural: alt data loaded, %d daily row(s), %d column(s).",
|
|
g_altData.Rows(), ALT_COLUMNS);
|
|
string names[];
|
|
ArrayResize(names, NEURAL_FEATURES);
|
|
for(int f = 0; f < NEURAL_FEATURES; f++)
|
|
names[f] = FeatureName(f);
|
|
//--- LOAD ONLY. Training is deferred to TrainIfDue() - see m_minBars. A refused load (wrong
|
|
//--- columns) is NOT retrained over silently: Load() has already said why, and quietly replacing
|
|
//--- a model the operator may be mid-way through evaluating would destroy what is being measured.
|
|
m_trained = m_net.Load(ModelPath(), names);
|
|
if(m_cut <= 0.50)
|
|
Print("CSignalNeural: confidence cut is 0.50 (no cut) - this module is INERT by design and "
|
|
"will abstain on every bar. Raise it to vote.");
|
|
if(m_trained)
|
|
Print("CSignalNeural: model loaded; no training this run.");
|
|
else
|
|
PrintFormat("CSignalNeural: no model yet (%s). Will train once %d bars of history exist,"
|
|
" then every %d bars - each fit uses only bars older than itself.",
|
|
m_net.Why(), m_minBars, m_retrainBars);
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Train when enough history has accrued, then every m_retrainBars. |
|
|
//| Called from the vote, so "now" is always the bar being decided and |
|
|
//| Bars() is always history-so-far. |
|
|
//+------------------------------------------------------------------+
|
|
void CSignalNeural::TrainIfDue(void)
|
|
{
|
|
const datetime bar = iTime(m_symbol.Name(), m_period, 0);
|
|
if(bar == m_lastTrainBar)
|
|
return; // at most one attempt per bar
|
|
const int bars = Bars(m_symbol.Name(), m_period);
|
|
if(bars < m_minBars)
|
|
return;
|
|
if(m_trainedAtBars > 0 && (m_retrainBars <= 0 || bars < m_trainedAtBars + m_retrainBars))
|
|
return;
|
|
m_lastTrainBar = bar;
|
|
//--- BACK OFF WHETHER IT SUCCEEDS OR FAILS. Without this a refusal retries on every single bar
|
|
//--- and prints the same sentence hundreds of times - which buries the one line that matters and
|
|
//--- costs a full history walk each time.
|
|
m_trainedAtBars = bars;
|
|
if(TrainFromHistory())
|
|
{
|
|
m_trained = true;
|
|
m_trainCount++;
|
|
}
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| WHAT A MODULE CAN ACTUALLY SAY, read out of the standard library |
|
|
//| rather than assumed (Include\Expert\ExpertSignal.mqh): |
|
|
//| |
|
|
//| Direction() is m_weight*(LongCondition()-ShortCondition()), |
|
|
//| line 431, and the root averages it over the number of filters |
|
|
//| that answered, line 458. So a module has exactly THREE moves: |
|
|
//| vote long, vote short, or return 0/0 - and 0/0 is not silence, |
|
|
//| the parent still counts it in `number` and it dilutes everyone |
|
|
//| else's vote. |
|
|
//| |
|
|
//| THERE IS NO PER-SIDE VETO. EMPTY_VALUE is not one. Returned by |
|
|
//| a filter it makes the root return EMPTY_VALUE immediately |
|
|
//| (line 449-450), and the top-level m_direction==EMPTY_VALUE |
|
|
//| makes CheckOpenLong, CheckOpenShort, CheckCloseLong AND |
|
|
//| CheckCloseShort all return false (lines 231, 254, 317, 340). |
|
|
//| It is the NO-ACTION state - the constructor initialises |
|
|
//| m_direction to EMPTY_VALUE as "nothing computed yet" (line |
|
|
//| 103). So one filter returning it silences all twelve others for |
|
|
//| that bar, and suppresses signal-driven exits too. |
|
|
//| |
|
|
//| I built the refusal that way and measured it: -98.49 over 46 |
|
|
//| trades against +81.30 over 60 with the module off. It is the |
|
|
//| bluntest instrument in the framework and the measurement does not |
|
|
//| ask for it - so it is gone. |
|
|
//| |
|
|
//| WHAT IS LEFT IS THE HONEST TENSION. The quintile study measured |
|
|
//| mean R BY QUINTILE TAKEN LONG: -0.230 / -0.089 / -0.026 / +0.002 |
|
|
//| / +0.017. Its only claim is "the bottom quintile is a bad LONG". |
|
|
//| Expressing that as a short vote assumes the payoff is |
|
|
//| antisymmetric - that what a long loses a short earns - which the |
|
|
//| study never tested and the spread contradicts. But the arithmetic |
|
|
//| above offers no way to say "not long" without saying "short": |
|
|
//| the same subtraction that pulls the average below the long |
|
|
//| threshold pushes it toward the short one. |
|
|
//| |
|
|
//| So the short vote stays, and it is labelled for what it is: an |
|
|
//| ASSUMPTION, not a measurement. Whether it pays is the question |
|
|
//| the multi-symbol sweep exists to answer, because at 60 trades the |
|
|
//| standard error on the mean is ~2.3 per trade and every result in |
|
|
//| this file so far - +81.30, +30.44, -98.49 - sits inside it. |
|
|
//+------------------------------------------------------------------+
|
|
double CSignalNeural::Score(void)
|
|
{
|
|
//--- CONF_50 MEANS OFF, and it has to mean off HERE or it means the opposite. The bands are
|
|
//--- `p >= cut` and `p <= 1-cut`; at cut = 0.50 those are `p >= 0.5` and `p <= 0.5`, one of
|
|
//--- which is true for EVERY p - so the module would speak on every bar instead of abstaining.
|
|
//--- A dead band at 0.50 is not a tidy-up: without it, "no cut" is the most opinionated setting.
|
|
if(!m_trained || m_cut <= 0.50)
|
|
return -1.0;
|
|
const datetime bar = iTime(m_symbol.Name(), m_period, 0);
|
|
if(bar == m_scoreBar)
|
|
return m_score;
|
|
m_scoreBar = bar;
|
|
m_score = -1.0;
|
|
double x[];
|
|
if(!BuildFeatures(x, 1))
|
|
return -1.0;
|
|
const double p = m_net.Score(x);
|
|
m_score = (p < 0.0) ? -1.0 : p;
|
|
return m_score;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
int CSignalNeural::LongCondition(void)
|
|
{
|
|
TrainIfDue();
|
|
const double p = Score();
|
|
//--- The confirmation, deliberately small: the top quintile measured +0.017 R, breakeven before
|
|
//--- costs. A module returning a big number here would claim an edge the measurement lacks.
|
|
if(p >= m_cut)
|
|
{
|
|
m_active_pattern = "Pattern_1";
|
|
m_active_direction = "Buy";
|
|
return m_pattern_1;
|
|
}
|
|
return 0;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
int CSignalNeural::ShortCondition(void)
|
|
{
|
|
//--- TrainIfDue() is NOT called here: LongCondition() runs first on every evaluation and has
|
|
//--- already called it. Score() is cached per bar, so both sides read the same number.
|
|
const double p = Score();
|
|
//--- THE ASSUMPTION, carrying the heavier weight because the bottom quintile is where the
|
|
//--- measured separation is. See the note above: "bad long" -> "good short" is a step the
|
|
//--- quintile study does not license, and this is the line that takes it.
|
|
if(p >= 0.0 && p <= 1.0 - m_cut)
|
|
{
|
|
m_active_pattern = "Pattern_0";
|
|
m_active_direction = "Sell";
|
|
return m_pattern_0;
|
|
}
|
|
return 0;
|
|
}
|
|
#endif // WARRIOR_SIGNALNEURAL_MQH
|