- 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.
218 lines
13 KiB
MQL5
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
|