2026-09-13 14:32:40 -04:00 | | | //+------------------------------------------------------------------+
|
| | | //| 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"
|
2026-09-30 18:36:33 -04:00 | | | #include "..\Mind\VolumeFeed.mqh"
|
| | | #include "..\System\FeatureScale.mqh"
|
2026-09-13 14:32:40 -04:00 | | |
|
| | | //--- 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.
|
2026-09-30 18:36:33 -04:00 | | | static double Squash(const double v) { return FeatSquash(v); }
|
2026-09-13 14:32:40 -04:00 | | | //--- 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
|
2026-09-30 18:36:33 -04:00 | | | //--- price. These come from g_volFeed: log volume against the SAME HOUR's norm, level-
|
| | | //--- detrended (Mind/VolumeProfile.mqh). The earlier "vs the last 20 bars" ratios mostly
|
| | | //--- encoded the hour of day. Renaming them is what stops an old model file loading.
|
| | | case 18: return "rvol_tod";
|
| | | case 19: return "rvol_tod_mean5";
|
| | | case 20: return "rvol_tod_chg1";
|
2026-09-13 14:32:40 -04:00 | | | //--- 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.
|
2026-09-30 18:36:33 -04:00 | | | double rvNow, rvMean5, rvChg;
|
| | | //--- A bar with no reading yet (the profile is warming up, or the bar is dead) refuses the row
|
| | | //--- rather than emit a constant the trainer would learn as "quiet".
|
| | | if(!g_volFeed.Reading(iTime(m_symbol.Name(), m_period, shift), rvNow, rvMean5, rvChg))
|
2026-09-13 14:32:40 -04:00 | | | return false;
|
2026-09-30 18:36:33 -04:00 | | | x[i++] = Squash(rvNow);
|
| | | x[i++] = Squash(rvMean5);
|
| | | x[i++] = Squash(rvChg);
|
2026-09-13 14:32:40 -04:00 | | |
|
| | | //--- 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.
|
2026-09-30 18:36:33 -04:00 | | | //--- The volume feed reads its own whole-history cache; bring it level with the sweep first.
|
| | | if(!g_volFeed.Sync())
|
| | | return false;
|
2026-09-13 14:32:40 -04:00 | | | 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;
|
| | | }
|
2026-09-30 18:36:33 -04:00 | | | g_volFeed.Ensure(m_symbol.Name(), m_period);
|
2026-09-13 14:32:40 -04:00 | | | 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[];
|
2026-09-30 18:36:33 -04:00 | | | if(!g_volFeed.Sync() || !BuildFeatures(x, 1))
|
2026-09-13 14:32:40 -04:00 | | | 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
|