//+------------------------------------------------------------------+ //| 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 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; //--- 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; public: CWarriorSignal(void) : m_id("?"), m_active_pattern(""), m_active_direction(""), m_pattern_count(0), m_confirm_mask(0) {} ~CWarriorSignal(void) {} string FilterID(void) const { return m_id; } int PatternCount(void) const { return m_pattern_count; } 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)); } 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) { } //--- 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; }; //+------------------------------------------------------------------+ 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; } #endif // WARRIOR_SIMPLE_SIGNAL_MQH