Warrior_EA/Expert/WarriorSignal.mqh
AnimateDread 47a5ef338b Refactor Warrior EA: Integrate custom signal modules, enhance voting mechanism, and improve management features
- 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.
2026-09-13 14:32:40 -04:00

218 lines
13 KiB
MQL5

//+------------------------------------------------------------------+
//| 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;
//--- 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