2026-09-11 06:16:08 -04:00 | | | //+------------------------------------------------------------------+
|
| | | //| WarriorSignal.mqh |
|
| | | //| AnimateDread |
|
| | | //| |
|
| | | //| THE THIN BASE. Everything a Warrior signal module needs on top of |
|
| | | //| the standard library, and nothing else. |
|
| | | //| |
|
| | | //| CExpertSignalCustom grew to ~4,000 lines because the vote, the |
|
| | | //| journal, the chart overlay, the arming machinery, the meta-label |
|
| | | //| and the per-setup order shaping all ended up inside the class the |
|
| | | //| indicator modules inherit. A module that only wants to say "RSI |
|
| | | //| is oversold, weight 30" paid for all of it, and could not be read |
|
| | | //| without reading all of it. |
|
| | | //| |
|
| | | //| Measured against the modules themselves (2026-09-11): beyond what |
|
| | | //| CExpertSignal and CExpertBase already give them - m_symbol, |
|
| | | //| m_period, m_used_series, m_patterns_usage, m_base_price, |
|
| | | //| StartIndex(), PriceHigh/Low/Open/Close - every classic module in |
|
| | | //| this repo uses exactly FOUR members of the old base. They are the |
|
| | | //| four below. That is the whole reason this file can exist. |
|
| | | //| |
|
| | | //| SO A MODULE IS JUST A CExpertSignal. It implements LongCondition |
|
| | | //| and ShortCondition returning 0..100, and the standard library |
|
| | | //| does the rest: Direction() weights it, the threshold decides, and |
|
| | | //| CExpert places the order. Exactly the shape MetaEditor's wizard |
|
| | | //| generates - which is the point. |
|
| | | //+------------------------------------------------------------------+
|
| | | #ifndef WARRIOR_SIMPLE_SIGNAL_MQH
|
| | | #define WARRIOR_SIMPLE_SIGNAL_MQH
|
| | |
|
| | | #include <Expert\ExpertSignal.mqh>
|
| | |
|
| | | class CWarriorSignal : public CExpertSignal
|
| | | {
|
| | | protected:
|
| | | //--- SHORT, STABLE, AND THE DATABASE KEY. Rows are grouped by this, so renaming one silently
|
| | | //--- splits a module's history in two. Treat it as an identifier, not a label.
|
| | | string m_id;
|
| | | //--- WHAT MATCHED ON THIS EVALUATION, for the journal. Set by the module inside LongCondition()/
|
| | | //--- ShortCondition(); read by the parent immediately after the call and then cleared. A module
|
| | | //--- that never sets it still votes - it simply records the vote without naming a pattern.
|
| | | string m_active_pattern;
|
| | | string m_active_direction;
|
| | | //--- How many distinct patterns this module can express. The database keeps one row group per
|
| | | //--- pattern, and the ranking averages within a pattern rather than across a module, because
|
| | | //--- "RSI" is not one behaviour - an oversold bounce and a divergence are different claims.
|
| | | int m_pattern_count;
|
2026-09-13 14:32:40 -04:00 | | | //--- CONFIRMATION PATTERNS - bit i set means pattern i is one. Bit, not a list, so the test is
|
| | | //--- free on the hot path where every firing is checked.
|
| | | //---
|
| | | //--- A confirmation pattern answers "is price on the bullish side of this indicator" rather than
|
| | | //--- "has something happened". It is true on roughly every bar, ships at weight 10 because it
|
| | | //--- must never trade on its own, and it is POISON to the ranking layer for two separate
|
| | | //--- reasons. It floods the journal - measured, CCI_Pattern_0_Sell alone held 1,139 rows against
|
| | | //--- 107 for a real trigger - and, being close to a coin flip, the database re-weights it from
|
| | | //--- its authored 10 up to ~50, a five-fold amplification of the one pattern that was
|
| | | //--- deliberately made quiet. So these are neither recorded nor re-weighted: they keep the
|
| | | //--- weight their author gave them, and the database never sees them.
|
| | | uint m_confirm_mask;
|
2026-09-11 06:16:08 -04:00 | | |
|
| | | public:
|
| | | CWarriorSignal(void) : m_id("?"), m_active_pattern(""),
|
2026-09-13 14:32:40 -04:00 | | | m_active_direction(""), m_pattern_count(0),
|
| | | m_confirm_mask(0) {}
|
2026-09-11 06:16:08 -04:00 | | | ~CWarriorSignal(void) {}
|
| | |
|
| | | string FilterID(void) const { return m_id; }
|
| | | int PatternCount(void) const { return m_pattern_count; }
|
2026-09-13 14:32:40 -04:00 | | | bool IsConfirmation(const int p) const
|
| | | { return (p >= 0 && p < 32 && ((m_confirm_mask >> p) & 1) != 0); }
|
| | | //--- "Pattern_7" -> 7, and -1 for anything that is not a pattern name. The journal and the
|
| | | //--- ranking both need the INDEX, and the only thing a module publishes is the string.
|
| | | static int PatternIndex(const string name)
|
| | | {
|
| | | if(StringLen(name) <= 8 || StringSubstr(name, 0, 8) != "Pattern_")
|
| | | return -1;
|
| | | return (int)StringToInteger(StringSubstr(name, 8));
|
| | | }
|
2026-09-11 06:16:08 -04:00 | | | string ActivePattern(void) const { return m_active_pattern; }
|
| | | string ActiveDirection(void) const { return m_active_direction; }
|
| | | void ClearActive(void) { m_active_pattern = ""; m_active_direction = ""; }
|
| | |
|
| | | //--- THE ONE HOOK THE DATABASE NEEDS. The ranking layer calls this to hand a module the weight it
|
| | | //--- earned for one of its patterns. Default does nothing, so a module that does not want to be
|
| | | //--- ranked simply does not override it - it keeps its fixed prior and stays in the vote.
|
| | | //--- Signature matches the ~20 existing modules exactly (no `const` on the value parameters).
|
| | | //--- MQL5 treats a const-qualified parameter as a different signature for override purposes and
|
| | | //--- warns rather than errors, so a mismatch here would leave every module's override silently
|
| | | //--- attached to a slightly different function.
|
| | | virtual void ApplyPatternWeight(int pattern, int weight) { }
|
2026-09-13 14:32:40 -04:00 | | |
|
| | | //--- GROW THE SHARED PRICE SERIES PAST THE STANDARD LIBRARY'S 1024-BAR CEILING.
|
| | | //---
|
| | | //--- CSeries allocates DEFAULT_BUFFER_SIZE = 1024 bars (Include\Indicators\Series.mqh:11), so
|
| | | //--- Close(shift) reads 0.0 for any shift past 1023 - SILENTLY. There is no error and no empty
|
| | | //--- value to test; a module that walks history just stops finding data, and whatever it was
|
| | | //--- building comes out short. Measured 2026-09-11: the neural module reported "915 rows" on
|
| | | //--- every one of its four training events across an 11-year run, on 3,029 bars of history.
|
| | | //--- 915 is not a coincidence or a window policy - it is 974 (the deepest shift whose 50-bar
|
| | | //--- SMA lookback still lands inside 1023) minus the module's 60-bar floor, plus one. The
|
| | | //--- stdlib default was choosing the training set, and through it the model's width.
|
| | | //---
|
| | | //--- ONLY EVER ASK FOR BARS THAT ALREADY EXIST. CSeries::BufferResize() loads history and
|
| | | //--- printf()s a failure line when it cannot supply the request, so passing an optimistic
|
| | | //--- constant here buys a log full of noise. Callers pass min(want, Bars()) - which in the
|
| | | //--- tester is history-so-far, so growing the buffer cannot reach past the bar being decided.
|
| | | //---
|
| | | //--- Grows only. The stdlib's own BufferResize() would happily SHRINK a buffer (its size>
|
| | | //--- guard covers the history load, not the buff.Size() call underneath), which would quietly
|
| | | //--- truncate a series another module is mid-way through reading.
|
| | | bool DeepenPrices(const int size);
|
| | |
|
| | | //--- WHICH GAME IS BEING PLAYED, on the shared base because TWO things need it and they must
|
| | | //--- agree: CSignalRegime votes on it, and CWarriorVote stamps it onto every journal row so a
|
| | | //--- pattern's record can be split by the conditions it was earned in. Two copies of this would
|
| | | //--- eventually disagree, and the disagreement would look like a regime change in the data.
|
| | | //---
|
| | | //--- EFFICIENCY RATIO - net distance over path walked. 1 = a straight line, 0 = thrash that ends
|
| | | //--- where it began. Needs no volatility estimate: a ratio of two distances in the same units
|
| | | //--- cancels the instrument, so one threshold means the same thing on gold and on EURUSD.
|
| | | double EfficiencyRatio(const int shift, const int period) const;
|
| | | //--- VARIANCE RATIO - Var(q-bar) / (q * Var(1-bar)). A random walk gives exactly 1.0 because
|
| | | //--- variance scales with time. Above 1 the moves compound (trend); below 1 they cancel (mean
|
| | | //--- reversion). Sharper than ER: ER says "is there a trend", VR says "does this CONTINUE".
|
| | | double VarianceRatio(const int shift, const int period, const int q) const;
|
| | | //--- The coarse label those two agree on. Deliberately few: split the journal too finely and
|
| | | //--- every cell falls under the sample floor, which is the same as having no ranking at all.
|
| | | int RegimeCode(const int shift) const;
|
2026-09-11 06:16:08 -04:00 | | | };
|
2026-09-13 14:32:40 -04:00 | | | //+------------------------------------------------------------------+
|
| | | double CWarriorSignal::EfficiencyRatio(const int shift, const int period) const
|
| | | {
|
| | | const double net = MathAbs(Close(shift) - Close(shift + period));
|
| | | double path = 0.0;
|
| | | for(int i = 0; i < period; i++)
|
| | | path += MathAbs(Close(shift + i) - Close(shift + i + 1));
|
| | | //--- A flat window has zero path AND zero net. That is not "perfectly efficient" - it is no
|
| | | //--- information, so it reads as chop rather than as a 0/0 that would look like a perfect trend.
|
| | | if(path <= 0.0)
|
| | | return 0.0;
|
| | | return net / path;
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | double CWarriorSignal::VarianceRatio(const int shift, const int period, const int q) const
|
| | | {
|
| | | if(period < q * 4 || q < 2)
|
| | | return 1.0; // too few independent blocks to estimate anything
|
| | | double m1 = 0.0;
|
| | | for(int i = 0; i < period; i++)
|
| | | m1 += (Close(shift + i) - Close(shift + i + 1));
|
| | | m1 /= period;
|
| | | double v1 = 0.0;
|
| | | for(int i = 0; i < period; i++)
|
| | | {
|
| | | const double d = (Close(shift + i) - Close(shift + i + 1)) - m1;
|
| | | v1 += d * d;
|
| | | }
|
| | | v1 /= (period - 1);
|
| | | if(v1 <= 0.0)
|
| | | return 1.0;
|
| | | const int blocks = period / q; // non-overlapping, so the blocks are independent
|
| | | double mq = 0.0;
|
| | | for(int b = 0; b < blocks; b++)
|
| | | mq += (Close(shift + b * q) - Close(shift + (b + 1) * q));
|
| | | mq /= blocks;
|
| | | double vq = 0.0;
|
| | | for(int b = 0; b < blocks; b++)
|
| | | {
|
| | | const double d = (Close(shift + b * q) - Close(shift + (b + 1) * q)) - mq;
|
| | | vq += d * d;
|
| | | }
|
| | | vq /= (blocks - 1);
|
| | | return vq / (q * v1);
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| 0 = consolidation, 1 = trending, 2 = mean reverting. |
|
| | | //| |
|
| | | //| THREE, NOT MORE. Every extra regime divides the journal again, and |
|
| | | //| a pattern needs WARRIOR_MIN_FIRINGS observations per cell before |
|
| | | //| it is ranked at all - so a finer split does not produce a sharper |
|
| | | //| estimate, it produces no estimate. Three is what ~15k firings per |
|
| | | //| symbol can actually support. |
|
| | | //+------------------------------------------------------------------+
|
| | | int CWarriorSignal::RegimeCode(const int shift) const
|
| | | {
|
| | | const double er = EfficiencyRatio(shift, 20);
|
| | | const double vr = VarianceRatio(shift, 60, 5);
|
| | | if(er >= 0.35 && vr >= 1.0)
|
| | | return 1; // efficient AND compounding: a trend
|
| | | if(vr <= 0.85 && er <= 0.15)
|
| | | return 2; // cancelling and inefficient: mean reverting
|
| | | return 0; // everything else is consolidation
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | bool CWarriorSignal::DeepenPrices(const int size)
|
| | | {
|
| | | if(size <= 0)
|
| | | return false;
|
| | | bool ok = true;
|
| | | if(CheckPointer(m_open) != POINTER_INVALID && size > m_open.BufferSize())
|
| | | ok = m_open.BufferResize(size) && ok;
|
| | | if(CheckPointer(m_high) != POINTER_INVALID && size > m_high.BufferSize())
|
| | | ok = m_high.BufferResize(size) && ok;
|
| | | if(CheckPointer(m_low) != POINTER_INVALID && size > m_low.BufferSize())
|
| | | ok = m_low.BufferResize(size) && ok;
|
| | | if(CheckPointer(m_close) != POINTER_INVALID && size > m_close.BufferSize())
|
| | | ok = m_close.BufferResize(size) && ok;
|
| | | //--- Tick volume is a CSeries too and carries the SAME 1024-bar ceiling. Left shallow it would
|
| | | //--- read 0 past shift 1023, and a zero volume denominator makes BuildFeatures refuse the row -
|
| | | //--- so the training set would silently stop at 1024 bars again, by a different route.
|
| | | if(CheckPointer(m_tick_volume) != POINTER_INVALID && size > m_tick_volume.BufferSize())
|
| | | ok = m_tick_volume.BufferResize(size) && ok;
|
| | | //--- Time is a CSeries with the same ceiling; a calendar feature read past it is a 1970 date.
|
| | | if(CheckPointer(m_time) != POINTER_INVALID && size > m_time.BufferSize())
|
| | | ok = m_time.BufferResize(size) && ok;
|
| | | return ok;
|
| | | }
|
2026-09-11 06:16:08 -04:00 | | | #endif // WARRIOR_SIMPLE_SIGNAL_MQH
|